vite-plugin-rpx 0.11.20 → 0.11.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +883 -228
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -22443,6 +22443,158 @@ var init_https = __esm(() => {
22443
22443
  SHARED_DEV_HOST_CERT_PATH = join12(homedir8(), ".stacks", "ssl", "rpx.localhost.crt");
22444
22444
  });
22445
22445
 
22446
+ // ../rpx/src/auth.ts
22447
+ import { createHash, timingSafeEqual } from "node:crypto";
22448
+ import { readFileSync as readFileSync2 } from "node:fs";
22449
+ function safeEqual(a2, b2) {
22450
+ const ab = Buffer.from(a2, "utf8");
22451
+ const bb = Buffer.from(b2, "utf8");
22452
+ if (ab.length !== bb.length) {
22453
+ timingSafeEqual(ab, ab);
22454
+ return false;
22455
+ }
22456
+ return timingSafeEqual(ab, bb);
22457
+ }
22458
+ function apr1Crypt(password, salt) {
22459
+ const md5 = (buf) => createHash("md5").update(buf).digest();
22460
+ const pw = Buffer.from(password, "utf8");
22461
+ const saltBuf = Buffer.from(salt, "utf8");
22462
+ const result = md5(Buffer.concat([pw, saltBuf, pw]));
22463
+ const ctx = [pw, Buffer.from("$apr1$", "utf8"), saltBuf];
22464
+ for (let i2 = pw.length;i2 > 0; i2 -= 16)
22465
+ ctx.push(result.subarray(0, Math.min(i2, 16)));
22466
+ for (let i2 = pw.length;i2 > 0; i2 >>= 1)
22467
+ ctx.push(i2 & 1 ? Buffer.from([0]) : pw.subarray(0, 1));
22468
+ let final = md5(Buffer.concat(ctx));
22469
+ for (let i2 = 0;i2 < 1000; i2++) {
22470
+ const round = [];
22471
+ round.push(i2 & 1 ? pw : final.subarray(0, 16));
22472
+ if (i2 % 3)
22473
+ round.push(saltBuf);
22474
+ if (i2 % 7)
22475
+ round.push(pw);
22476
+ round.push(i2 & 1 ? final.subarray(0, 16) : pw);
22477
+ final = md5(Buffer.concat(round));
22478
+ }
22479
+ const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
22480
+ const to64 = (value, count) => {
22481
+ let out = "";
22482
+ let v2 = value;
22483
+ for (let i2 = 0;i2 < count; i2++) {
22484
+ out += itoa64[v2 & 63];
22485
+ v2 >>= 6;
22486
+ }
22487
+ return out;
22488
+ };
22489
+ const p2 = final;
22490
+ let encoded = "";
22491
+ encoded += to64(p2[0] << 16 | p2[6] << 8 | p2[12], 4);
22492
+ encoded += to64(p2[1] << 16 | p2[7] << 8 | p2[13], 4);
22493
+ encoded += to64(p2[2] << 16 | p2[8] << 8 | p2[14], 4);
22494
+ encoded += to64(p2[3] << 16 | p2[9] << 8 | p2[15], 4);
22495
+ encoded += to64(p2[4] << 16 | p2[10] << 8 | p2[5], 4);
22496
+ encoded += to64(p2[11], 2);
22497
+ return `$apr1$${salt}$${encoded}`;
22498
+ }
22499
+ function verifyHtpasswdHash(password, hash) {
22500
+ if (/^\$2[aby]?\$/.test(hash)) {
22501
+ try {
22502
+ return Bun.password.verifySync(password, hash);
22503
+ } catch {
22504
+ return false;
22505
+ }
22506
+ }
22507
+ if (hash.startsWith("$apr1$")) {
22508
+ const parts = hash.split("$");
22509
+ const salt = parts[2] ?? "";
22510
+ return safeEqual(apr1Crypt(password, salt), hash);
22511
+ }
22512
+ if (hash.startsWith("{SHA}")) {
22513
+ const digest = createHash("sha1").update(password, "utf8").digest("base64");
22514
+ return safeEqual(`{SHA}${digest}`, hash);
22515
+ }
22516
+ return safeEqual(password, hash);
22517
+ }
22518
+ function parseHtpasswd(body) {
22519
+ const entries = new Map;
22520
+ for (const rawLine of body.split(`
22521
+ `)) {
22522
+ const line = rawLine.trim();
22523
+ if (!line || line.startsWith("#"))
22524
+ continue;
22525
+ const idx = line.indexOf(":");
22526
+ if (idx <= 0)
22527
+ continue;
22528
+ entries.set(line.slice(0, idx), line.slice(idx + 1));
22529
+ }
22530
+ return entries;
22531
+ }
22532
+ function resolveAuth(cfg) {
22533
+ if (!cfg)
22534
+ return;
22535
+ const realm = (cfg.realm ?? "").trim() || "Restricted";
22536
+ const inline = [];
22537
+ if (cfg.username)
22538
+ inline.push({ username: cfg.username, password: cfg.password ?? "" });
22539
+ for (const user of cfg.users ?? []) {
22540
+ if (user?.username)
22541
+ inline.push({ username: user.username, password: user.password ?? "" });
22542
+ }
22543
+ let htpasswd;
22544
+ if (cfg.htpasswdFile) {
22545
+ try {
22546
+ htpasswd = parseHtpasswd(readFileSync2(cfg.htpasswdFile, "utf8"));
22547
+ } catch {
22548
+ htpasswd = undefined;
22549
+ }
22550
+ }
22551
+ if (inline.length === 0 && (!htpasswd || htpasswd.size === 0))
22552
+ return;
22553
+ const verify = (username, password) => {
22554
+ let ok = false;
22555
+ for (const cred of inline) {
22556
+ if (safeEqual(username, cred.username) && safeEqual(password, cred.password))
22557
+ ok = true;
22558
+ }
22559
+ if (htpasswd) {
22560
+ const hash = htpasswd.get(username);
22561
+ if (hash && verifyHtpasswdHash(password, hash))
22562
+ ok = true;
22563
+ }
22564
+ return ok;
22565
+ };
22566
+ return { realm, verify };
22567
+ }
22568
+ function enforceBasicAuth(req, pathname, auth) {
22569
+ if (pathname.startsWith(ACME_CHALLENGE_PREFIX))
22570
+ return;
22571
+ const header = req.headers.get("authorization") ?? "";
22572
+ const sep = header.indexOf(" ");
22573
+ const scheme = sep === -1 ? header : header.slice(0, sep);
22574
+ const encoded = sep === -1 ? "" : header.slice(sep + 1).trim();
22575
+ if (scheme.toLowerCase() === "basic" && encoded) {
22576
+ let decoded = "";
22577
+ try {
22578
+ decoded = Buffer.from(encoded, "base64").toString("utf8");
22579
+ } catch {
22580
+ decoded = "";
22581
+ }
22582
+ const idx = decoded.indexOf(":");
22583
+ if (idx >= 0 && auth.verify(decoded.slice(0, idx), decoded.slice(idx + 1)))
22584
+ return;
22585
+ }
22586
+ const realm = auth.realm.replace(/["\\]/g, "");
22587
+ return new Response("401 Unauthorized", {
22588
+ status: 401,
22589
+ headers: {
22590
+ "www-authenticate": `Basic realm="${realm}", charset="UTF-8"`,
22591
+ "content-type": "text/plain; charset=utf-8"
22592
+ }
22593
+ });
22594
+ }
22595
+ var ACME_CHALLENGE_PREFIX = "/.well-known/acme-challenge/";
22596
+ var init_auth = () => {};
22597
+
22446
22598
  // ../rpx/src/proxy-pool.ts
22447
22599
  function isIdempotent(method) {
22448
22600
  return IDEMPOTENT_METHODS.has(method.toUpperCase());
@@ -22613,6 +22765,7 @@ class UpstreamPool {
22613
22765
  host;
22614
22766
  port;
22615
22767
  maxTotal;
22768
+ key;
22616
22769
  idle = [];
22617
22770
  waiters = [];
22618
22771
  open = 0;
@@ -22621,10 +22774,11 @@ class UpstreamPool {
22621
22774
  maxWaiters;
22622
22775
  checkoutIdleMs = checkoutIdleMs();
22623
22776
  inUse = new Set;
22624
- constructor(host, port, maxTotal) {
22777
+ constructor(host, port, maxTotal, key = "") {
22625
22778
  this.host = host;
22626
22779
  this.port = port;
22627
22780
  this.maxTotal = maxTotal;
22781
+ this.key = key;
22628
22782
  this.maxWaiters = maxQueued(maxTotal);
22629
22783
  }
22630
22784
  trackCheckout(conn) {
@@ -22783,6 +22937,8 @@ class UpstreamPool {
22783
22937
  if (this.idle.length === 0 && this.inUse.size === 0 && this.sweeper) {
22784
22938
  clearInterval(this.sweeper);
22785
22939
  this.sweeper = null;
22940
+ if (this.key && pools.get(this.key) === this)
22941
+ pools.delete(this.key);
22786
22942
  }
22787
22943
  }
22788
22944
  }
@@ -22792,7 +22948,7 @@ function poolFor(hostPort, maxPerHost) {
22792
22948
  const idx = hostPort.lastIndexOf(":");
22793
22949
  const host = idx === -1 ? hostPort : hostPort.slice(0, idx);
22794
22950
  const port = idx === -1 ? 80 : Number(hostPort.slice(idx + 1));
22795
- pool = new UpstreamPool(host, port, maxPerHost);
22951
+ pool = new UpstreamPool(host, port, maxPerHost, hostPort);
22796
22952
  pools.set(hostPort, pool);
22797
22953
  }
22798
22954
  return pool;
@@ -23231,6 +23387,27 @@ var init_proxy_pool = __esm(() => {
23231
23387
  pools = new Map;
23232
23388
  });
23233
23389
 
23390
+ // ../rpx/src/redirect.ts
23391
+ function resolveRedirect(input) {
23392
+ const cfg = typeof input === "string" ? { to: input } : input;
23393
+ let to = (cfg.to ?? "").trim();
23394
+ if (to && !/^[a-z][\w+.-]*:\/\//i.test(to))
23395
+ to = `https://${to}`;
23396
+ to = to.replace(/\/+$/, "");
23397
+ return {
23398
+ to,
23399
+ status: cfg.status ?? DEFAULT_REDIRECT_STATUS,
23400
+ preservePath: cfg.preservePath ?? true
23401
+ };
23402
+ }
23403
+ function buildRedirectLocation(redirect, pathname, search) {
23404
+ if (!redirect.preservePath)
23405
+ return redirect.to || "/";
23406
+ const tail = `${pathname}${search}`;
23407
+ return tail === "/" ? `${redirect.to}/` : `${redirect.to}${tail}`;
23408
+ }
23409
+ var DEFAULT_REDIRECT_STATUS = 301;
23410
+
23234
23411
  // ../rpx/src/static-files.ts
23235
23412
  import * as path2 from "node:path";
23236
23413
  function resolveStaticRoute(cfg, cleanUrls) {
@@ -23346,6 +23523,9 @@ var init_static_files = __esm(() => {
23346
23523
  });
23347
23524
 
23348
23525
  // ../rpx/src/proxy-handler.ts
23526
+ function wsPendingCapBytes() {
23527
+ return Math.max(0, Number(process.env.RPX_MAX_WS_PENDING_BYTES)) || 8 * 1024 * 1024;
23528
+ }
23349
23529
  function extractHostname2(req) {
23350
23530
  const hostHeader = req.headers.get("host") || "";
23351
23531
  const colon = hostHeader.indexOf(":");
@@ -23384,15 +23564,34 @@ function splitPathQuery(rawUrl) {
23384
23564
  return { pathname: rawUrl.slice(pathStart), search: "" };
23385
23565
  return { pathname: rawUrl.slice(pathStart, q), search: rawUrl.slice(q) };
23386
23566
  }
23387
- function createProxyFetchHandler(getRoute, verbose) {
23567
+ function createProxyFetchHandler(getRoute, verbose, onNoRoute) {
23388
23568
  const inner = async (req, server) => {
23389
23569
  const { pathname, search } = splitPathQuery(req.url);
23390
23570
  const hostname = extractHostname2(req);
23391
- const route = getRoute(hostname, pathname);
23571
+ let route = getRoute(hostname, pathname);
23572
+ if (!route && onNoRoute) {
23573
+ const outcome = await onNoRoute(hostname, pathname, req);
23574
+ if (outcome instanceof Response)
23575
+ return outcome;
23576
+ if (outcome && outcome.retry)
23577
+ route = getRoute(hostname, pathname);
23578
+ }
23392
23579
  if (!route) {
23393
23580
  debugLog("request", `No route found for host: ${hostname}`, verbose);
23394
23581
  return new Response(`No proxy configured for ${hostname}`, { status: 404 });
23395
23582
  }
23583
+ if (route.auth) {
23584
+ const challenge = enforceBasicAuth(req, pathname, route.auth);
23585
+ if (challenge) {
23586
+ debugLog("request", `401 challenge for ${hostname}${pathname}`, verbose);
23587
+ return challenge;
23588
+ }
23589
+ }
23590
+ if (route.redirect) {
23591
+ const location = buildRedirectLocation(route.redirect, pathname, search);
23592
+ debugLog("request", `${route.redirect.status} redirect ${hostname}${pathname} → ${location}`, verbose);
23593
+ return new Response(null, { status: route.redirect.status, headers: { location } });
23594
+ }
23396
23595
  if (route.static) {
23397
23596
  const strip = route.stripBasePathPrefix ?? true;
23398
23597
  const staticPath = strip ? stripBasePath(pathname, route.basePath) : pathname;
@@ -23486,6 +23685,7 @@ function createProxyFetchHandler(getRoute, verbose) {
23486
23685
  }
23487
23686
  function createProxyWebSocketHandler(verbose) {
23488
23687
  const state = new WeakMap;
23688
+ const maxPendingBytes = wsPendingCapBytes();
23489
23689
  return {
23490
23690
  open(ws) {
23491
23691
  const { targetUrl, forwardHeaders } = ws.data;
@@ -23498,13 +23698,14 @@ function createProxyWebSocketHandler(verbose) {
23498
23698
  return;
23499
23699
  }
23500
23700
  upstream.binaryType = "arraybuffer";
23501
- const st = { upstream, upstreamOpen: false, pending: [] };
23701
+ const st = { upstream, upstreamOpen: false, pending: [], pendingBytes: 0 };
23502
23702
  state.set(ws, st);
23503
23703
  upstream.addEventListener("open", () => {
23504
23704
  st.upstreamOpen = true;
23505
23705
  for (const frame of st.pending)
23506
23706
  upstream.send(frame);
23507
23707
  st.pending = [];
23708
+ st.pendingBytes = 0;
23508
23709
  });
23509
23710
  upstream.addEventListener("message", (ev) => {
23510
23711
  ws.send(ev.data);
@@ -23526,10 +23727,24 @@ function createProxyWebSocketHandler(verbose) {
23526
23727
  if (!st)
23527
23728
  return;
23528
23729
  const frame = typeof message === "string" ? message : new Uint8Array(message);
23529
- if (st.upstreamOpen)
23730
+ if (st.upstreamOpen) {
23530
23731
  st.upstream.send(frame);
23531
- else
23532
- st.pending.push(frame);
23732
+ return;
23733
+ }
23734
+ const size = typeof frame === "string" ? Buffer.byteLength(frame) : frame.byteLength;
23735
+ if (st.pendingBytes + size > maxPendingBytes) {
23736
+ debugLog("ws", `pending buffer would exceed ${maxPendingBytes}B before upstream open; closing socket`, verbose);
23737
+ try {
23738
+ ws.close(1009, "buffer limit");
23739
+ } catch {}
23740
+ try {
23741
+ st.upstream.close(1011, "client buffer overflow");
23742
+ } catch {}
23743
+ state.delete(ws);
23744
+ return;
23745
+ }
23746
+ st.pending.push(frame);
23747
+ st.pendingBytes += size;
23533
23748
  },
23534
23749
  close(ws, code, reason) {
23535
23750
  const st = state.get(ws);
@@ -23544,6 +23759,7 @@ function createProxyWebSocketHandler(verbose) {
23544
23759
  }
23545
23760
  var HOP_BY_HOP;
23546
23761
  var init_proxy_handler = __esm(() => {
23762
+ init_auth();
23547
23763
  init_proxy_pool();
23548
23764
  init_static_files();
23549
23765
  init_utils();
@@ -23875,6 +24091,144 @@ var init_on_demand = __esm(() => {
23875
24091
  init_utils();
23876
24092
  });
23877
24093
 
24094
+ // ../rpx/src/site-resolver.ts
24095
+ var DEFAULT_IDLE_TIMEOUT_MS;
24096
+ var init_site_resolver = __esm(() => {
24097
+ DEFAULT_IDLE_TIMEOUT_MS = 30 * 60000;
24098
+ });
24099
+
24100
+ // ../rpx/src/port-manager.ts
24101
+ import * as net from "node:net";
24102
+ function isPortInUse(port, hostname, verbose) {
24103
+ debugLog("port", `Checking if port ${port} is in use on ${hostname}`, verbose);
24104
+ return new Promise((resolve14) => {
24105
+ const server = net.createServer();
24106
+ const timeout = setTimeout(() => {
24107
+ debugLog("port", `Checking port ${port} timed out, assuming it's in use`, verbose);
24108
+ server.close();
24109
+ resolve14(true);
24110
+ }, 3000);
24111
+ server.once("error", (err) => {
24112
+ clearTimeout(timeout);
24113
+ if (err.code === "EADDRINUSE") {
24114
+ debugLog("port", `Port ${port} is in use`, verbose);
24115
+ resolve14(true);
24116
+ } else {
24117
+ debugLog("port", `Error checking port ${port}: ${err.message}`, verbose);
24118
+ resolve14(true);
24119
+ }
24120
+ });
24121
+ server.once("listening", () => {
24122
+ clearTimeout(timeout);
24123
+ debugLog("port", `Port ${port} is available`, verbose);
24124
+ server.close();
24125
+ resolve14(false);
24126
+ });
24127
+ try {
24128
+ server.listen(port, hostname);
24129
+ } catch (err) {
24130
+ clearTimeout(timeout);
24131
+ debugLog("port", `Exception checking port ${port}: ${err}`, verbose);
24132
+ resolve14(true);
24133
+ }
24134
+ });
24135
+ }
24136
+ async function findAvailablePort(startPort, hostname, verbose, maxAttempts = 50) {
24137
+ debugLog("port", `Finding available port starting from ${startPort} (max attempts: ${maxAttempts})`, verbose);
24138
+ let port = startPort;
24139
+ let attempts = 0;
24140
+ while (attempts < maxAttempts) {
24141
+ attempts++;
24142
+ const isInUse = await isPortInUse(port, hostname, verbose);
24143
+ if (!isInUse) {
24144
+ debugLog("port", `Found available port: ${port} after ${attempts} attempts`, verbose);
24145
+ return port;
24146
+ }
24147
+ debugLog("port", `Port ${port} is in use, trying ${port + 1} (attempt ${attempts}/${maxAttempts})`, verbose);
24148
+ port++;
24149
+ }
24150
+ throw new Error(`Unable to find available port after ${maxAttempts} attempts starting from ${startPort}`);
24151
+ }
24152
+ function testPortConnectivity(port, hostname, timeout = 5000, verbose) {
24153
+ debugLog("port", `Testing connection to ${hostname}:${port}`, verbose);
24154
+ return new Promise((resolve14) => {
24155
+ const socket = net.connect({
24156
+ host: hostname,
24157
+ port,
24158
+ timeout
24159
+ });
24160
+ socket.once("connect", () => {
24161
+ debugLog("port", `Successfully connected to ${hostname}:${port}`, verbose);
24162
+ socket.end();
24163
+ resolve14(true);
24164
+ });
24165
+ socket.once("timeout", () => {
24166
+ debugLog("port", `Connection to ${hostname}:${port} timed out`, verbose);
24167
+ socket.destroy();
24168
+ resolve14(false);
24169
+ });
24170
+ socket.once("error", (err) => {
24171
+ debugLog("port", `Failed to connect to ${hostname}:${port}: ${err.message}`, verbose);
24172
+ socket.destroy();
24173
+ resolve14(false);
24174
+ });
24175
+ });
24176
+ }
24177
+
24178
+ class DefaultPortManager {
24179
+ usedPorts = new Set;
24180
+ hostname;
24181
+ verbose;
24182
+ maxRetries;
24183
+ constructor(hostname = "0.0.0.0", verbose, maxRetries = 50) {
24184
+ this.hostname = hostname;
24185
+ this.verbose = verbose;
24186
+ this.maxRetries = maxRetries;
24187
+ }
24188
+ async getNextAvailablePort(startPort, testConnectivity = false) {
24189
+ if (this.usedPorts.has(startPort)) {
24190
+ return this.findNextAvailablePort(startPort + 1, testConnectivity);
24191
+ }
24192
+ const isInUse = await isPortInUse(startPort, this.hostname, this.verbose);
24193
+ if (isInUse) {
24194
+ return this.findNextAvailablePort(startPort + 1, testConnectivity);
24195
+ }
24196
+ if (testConnectivity) {
24197
+ const isConnectable = await testPortConnectivity(startPort, this.hostname, 3000, this.verbose);
24198
+ if (!isConnectable) {
24199
+ debugLog("port", `Port ${startPort} is available but not connectable, trying next port`, this.verbose);
24200
+ return this.findNextAvailablePort(startPort + 1, testConnectivity);
24201
+ }
24202
+ }
24203
+ this.usedPorts.add(startPort);
24204
+ return startPort;
24205
+ }
24206
+ async findNextAvailablePort(startPort, testConnectivity = false) {
24207
+ const port = await findAvailablePort(startPort, this.hostname, this.verbose, this.maxRetries);
24208
+ if (testConnectivity) {
24209
+ const isConnectable = await testPortConnectivity(port, this.hostname, 3000, this.verbose);
24210
+ if (!isConnectable) {
24211
+ if (port < startPort + this.maxRetries) {
24212
+ return this.findNextAvailablePort(port + 1, testConnectivity);
24213
+ } else {
24214
+ throw new Error(`Unable to find a connectable port after ${this.maxRetries} attempts`);
24215
+ }
24216
+ }
24217
+ }
24218
+ this.usedPorts.add(port);
24219
+ return port;
24220
+ }
24221
+ releasePort(port) {
24222
+ debugLog("port", `Releasing port ${port}`, this.verbose);
24223
+ this.usedPorts.delete(port);
24224
+ }
24225
+ }
24226
+ var portManager;
24227
+ var init_port_manager = __esm(() => {
24228
+ init_utils();
24229
+ portManager = new DefaultPortManager;
24230
+ });
24231
+
23878
24232
  // ../rpx/src/registry.ts
23879
24233
  import * as fsp3 from "node:fs/promises";
23880
24234
  import { homedir as homedir9 } from "node:os";
@@ -23946,19 +24300,399 @@ var init_registry = __esm(() => {
23946
24300
  ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
23947
24301
  });
23948
24302
 
23949
- // ../rpx/src/dns-state.ts
24303
+ // ../rpx/src/site-supervisor.ts
24304
+ import { closeSync as closeSync6, openSync as openSync6, readFileSync as readFileSync3 } from "node:fs";
23950
24305
  import * as fsp4 from "node:fs/promises";
24306
+ import { spawn } from "node:child_process";
23951
24307
  import { homedir as homedir10 } from "node:os";
23952
24308
  import * as path6 from "node:path";
24309
+ import * as process25 from "node:process";
24310
+
24311
+ class SiteSupervisor {
24312
+ resolver;
24313
+ registryDir;
24314
+ rpxDir;
24315
+ verbose;
24316
+ startupTimeoutMs;
24317
+ pollIntervalMs;
24318
+ restartDelayMs;
24319
+ killGraceMs;
24320
+ launch;
24321
+ probePort;
24322
+ pickPort;
24323
+ isHostRoutable;
24324
+ onSiteActivating;
24325
+ now;
24326
+ writeEntry;
24327
+ removeEntry;
24328
+ sites = new Map;
24329
+ reaper;
24330
+ stopped = false;
24331
+ constructor(opts) {
24332
+ this.resolver = opts.resolver;
24333
+ this.registryDir = opts.registryDir ?? getRegistryDir();
24334
+ this.rpxDir = opts.rpxDir ?? path6.join(homedir10(), ".stacks", "rpx");
24335
+ this.verbose = opts.verbose ?? false;
24336
+ this.startupTimeoutMs = opts.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
24337
+ this.pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
24338
+ this.restartDelayMs = opts.restartDelayMs ?? DEFAULT_RESTART_DELAY_MS;
24339
+ this.killGraceMs = opts.killGraceMs ?? KILL_GRACE_MS;
24340
+ this.launch = opts.launcher ?? makeDefaultLauncher(this.verbose);
24341
+ this.probePort = opts.probePort ?? defaultReadinessProbe;
24342
+ this.pickPort = opts.pickPort ?? ((preferred) => findAvailablePort(preferred, "127.0.0.1"));
24343
+ this.isHostRoutable = opts.isHostRoutable ?? (() => false);
24344
+ this.onSiteActivating = opts.onSiteActivating;
24345
+ this.now = opts.now ?? Date.now;
24346
+ this.writeEntry = opts.writeEntry ?? writeEntry;
24347
+ this.removeEntry = opts.removeEntry ?? removeEntry;
24348
+ const reapInterval = opts.reapIntervalMs ?? DEFAULT_REAP_INTERVAL_MS;
24349
+ this.reaper = setInterval(() => {
24350
+ this.reapIdle();
24351
+ }, reapInterval);
24352
+ if (typeof this.reaper.unref === "function")
24353
+ this.reaper.unref();
24354
+ }
24355
+ async onRequest(host) {
24356
+ if (this.stopped)
24357
+ return { kind: "unknown" };
24358
+ let state = this.sites.get(host);
24359
+ if (state && state.status === "failed" && this.now() - state.failedAt >= this.restartDelayMs) {
24360
+ this.sites.delete(host);
24361
+ state = undefined;
24362
+ }
24363
+ if (!state) {
24364
+ const site = this.resolver.resolve(host);
24365
+ if (!site)
24366
+ return { kind: "unknown" };
24367
+ state = await this.start(site);
24368
+ }
24369
+ state.lastAccess = this.now();
24370
+ if (state.status === "ready")
24371
+ return { kind: "ready", host };
24372
+ if (state.status === "failed")
24373
+ return { kind: "failed", host, error: state.error ?? "failed to start", logTail: this.readLogTail(state) };
24374
+ return { kind: "starting", host, sinceMs: this.now() - state.startedAt, source: state.site.source, logTail: this.readLogTail(state, 16) };
24375
+ }
24376
+ async start(site) {
24377
+ try {
24378
+ this.onSiteActivating?.(site.host);
24379
+ } catch {}
24380
+ const ports = new Map;
24381
+ if (!site.selfRegisters) {
24382
+ for (const route of site.routes) {
24383
+ if (ports.has(route.portEnv))
24384
+ continue;
24385
+ const port = await this.pickPort(route.defaultPort ?? 3000);
24386
+ ports.set(route.portEnv, port);
24387
+ }
24388
+ }
24389
+ const logPath = path6.join(this.rpxDir, "sites", `${site.id}.log`);
24390
+ await fsp4.mkdir(path6.dirname(logPath), { recursive: true }).catch(() => {});
24391
+ const env2 = this.buildEnv(site, ports);
24392
+ let handle = null;
24393
+ let startError;
24394
+ try {
24395
+ handle = this.launch({ command: site.command, cwd: site.dir, env: env2, logPath });
24396
+ } catch (err) {
24397
+ startError = `failed to spawn: ${err.message}`;
24398
+ }
24399
+ const state = {
24400
+ site,
24401
+ status: startError ? "failed" : "starting",
24402
+ handle,
24403
+ ports,
24404
+ routeIds: [],
24405
+ startedAt: this.now(),
24406
+ lastAccess: this.now(),
24407
+ failedAt: startError ? this.now() : 0,
24408
+ error: startError,
24409
+ logPath,
24410
+ exited: false,
24411
+ ready: Promise.resolve()
24412
+ };
24413
+ this.sites.set(site.host, state);
24414
+ if (startError) {
24415
+ log.warn(`rpx: site ${site.host} ${startError}`);
24416
+ return state;
24417
+ }
24418
+ log.info(`rpx: booting ${site.host} → ${site.command} (${site.dir})`);
24419
+ handle.exited.then((code) => this.onProcessExit(state, code)).catch(() => {});
24420
+ state.ready = this.driveReadiness(state).catch((err) => {
24421
+ debugLog("sites", `readiness loop for ${site.host} threw: ${err}`, this.verbose);
24422
+ });
24423
+ return state;
24424
+ }
24425
+ async onProcessExit(state, code) {
24426
+ state.exited = true;
24427
+ if (this.stopped || this.sites.get(state.site.host) !== state)
24428
+ return;
24429
+ if (state.status === "ready") {
24430
+ log.warn(`rpx: ${state.site.host} exited${code !== null ? ` (code ${code})` : ""} — will reboot on next request`);
24431
+ this.sites.delete(state.site.host);
24432
+ for (const id of state.routeIds)
24433
+ await this.removeEntry(id, this.registryDir, this.verbose).catch(() => {});
24434
+ } else if (state.status === "starting") {
24435
+ this.fail(state, `process exited${code !== null ? ` with code ${code}` : ""} before becoming ready`);
24436
+ }
24437
+ }
24438
+ async driveReadiness(state) {
24439
+ const { site } = state;
24440
+ const deadline = this.now() + this.startupTimeoutMs;
24441
+ const ready = site.selfRegisters ? () => this.isHostRoutable(site.host) : await this.makeGateProbe(site, state.ports);
24442
+ while (this.now() < deadline && !this.stopped) {
24443
+ if (state.status !== "starting" || state.exited)
24444
+ return;
24445
+ if (await ready()) {
24446
+ await this.publishRoutes(state);
24447
+ if (state.status === "starting") {
24448
+ state.status = "ready";
24449
+ log.success(`rpx: ${site.host} ready`);
24450
+ }
24451
+ return;
24452
+ }
24453
+ await delay(this.pollIntervalMs);
24454
+ }
24455
+ if (state.status === "starting")
24456
+ this.fail(state, `did not become ready within ${Math.round(this.startupTimeoutMs / 1000)}s`);
24457
+ }
24458
+ async makeGateProbe(site, ports) {
24459
+ const gatePorts = site.routes.filter((r2) => r2.readyGate ?? normalizePathPrefix(r2.path) === "/").map((r2) => ports.get(r2.portEnv)).filter((p2) => typeof p2 === "number");
24460
+ const probePorts = gatePorts.length > 0 ? gatePorts : [...ports.values()];
24461
+ return async () => {
24462
+ for (const port of probePorts) {
24463
+ if (!await this.probePort(port))
24464
+ return false;
24465
+ }
24466
+ return probePorts.length > 0;
24467
+ };
24468
+ }
24469
+ async publishRoutes(state) {
24470
+ const { site, ports } = state;
24471
+ if (site.selfRegisters || site.routes.length === 0)
24472
+ return;
24473
+ const defaultRoute = site.routes.find((r2) => normalizePathPrefix(r2.path) === "/") ?? site.routes[0];
24474
+ const fromPort = ports.get(defaultRoute.portEnv);
24475
+ if (fromPort === undefined)
24476
+ return;
24477
+ const pathRewrites = [];
24478
+ for (const route of site.routes) {
24479
+ if (route === defaultRoute)
24480
+ continue;
24481
+ const port = ports.get(route.portEnv);
24482
+ if (port === undefined)
24483
+ continue;
24484
+ pathRewrites.push({
24485
+ from: normalizePathPrefix(route.path),
24486
+ to: `localhost:${port}`,
24487
+ stripPrefix: route.stripPrefix ?? false
24488
+ });
24489
+ }
24490
+ const entry = {
24491
+ id: site.id,
24492
+ from: `localhost:${fromPort}`,
24493
+ to: site.host,
24494
+ cwd: site.dir,
24495
+ createdAt: new Date(this.now()).toISOString(),
24496
+ pathRewrites: pathRewrites.length > 0 ? pathRewrites : undefined,
24497
+ pid: state.handle?.pid
24498
+ };
24499
+ await this.writeEntry(entry, this.registryDir, this.verbose);
24500
+ state.routeIds = [site.id];
24501
+ const tableDeadline = this.now() + 2000;
24502
+ while (this.now() < tableDeadline && !this.isHostRoutable(site.host)) {
24503
+ await delay(50);
24504
+ }
24505
+ }
24506
+ fail(state, error) {
24507
+ state.status = "failed";
24508
+ state.error = error;
24509
+ state.failedAt = this.now();
24510
+ log.warn(`rpx: site ${state.site.host} failed — ${error}`);
24511
+ this.killProcess(state);
24512
+ }
24513
+ buildEnv(site, ports) {
24514
+ const env2 = {};
24515
+ for (const [k2, v2] of Object.entries(process25.env)) {
24516
+ if (typeof v2 === "string")
24517
+ env2[k2] = v2;
24518
+ }
24519
+ Object.assign(env2, site.env);
24520
+ for (const [name, port] of ports)
24521
+ env2[name] = String(port);
24522
+ env2.RPX_SITE_HOST = site.host;
24523
+ env2.RPX_SITE_URL = `https://${site.host}`;
24524
+ return env2;
24525
+ }
24526
+ readLogTail(state, lines = 40) {
24527
+ try {
24528
+ const text = readFileSync3(state.logPath, "utf8");
24529
+ return text.split(`
24530
+ `).slice(-lines).join(`
24531
+ `).trim();
24532
+ } catch {
24533
+ return "";
24534
+ }
24535
+ }
24536
+ async stop(host) {
24537
+ const state = this.sites.get(host);
24538
+ if (!state)
24539
+ return;
24540
+ this.sites.delete(host);
24541
+ for (const id of state.routeIds)
24542
+ await this.removeEntry(id, this.registryDir, this.verbose).catch(() => {});
24543
+ await this.killProcess(state);
24544
+ log.info(`rpx: stopped ${host}`);
24545
+ }
24546
+ async killProcess(state) {
24547
+ const handle = state.handle;
24548
+ if (!handle)
24549
+ return;
24550
+ state.handle = null;
24551
+ try {
24552
+ handle.stop("SIGTERM");
24553
+ } catch {}
24554
+ const timer = setTimeout(() => {
24555
+ try {
24556
+ handle.stop("SIGKILL");
24557
+ } catch {}
24558
+ }, this.killGraceMs);
24559
+ if (typeof timer.unref === "function")
24560
+ timer.unref();
24561
+ await Promise.race([handle.exited.catch(() => {}), delay(this.killGraceMs + 500)]);
24562
+ clearTimeout(timer);
24563
+ }
24564
+ async reapIdle() {
24565
+ if (this.stopped)
24566
+ return;
24567
+ const now = this.now();
24568
+ for (const [host, state] of this.sites) {
24569
+ const idle = state.site.idleTimeoutMs;
24570
+ if (idle <= 0)
24571
+ continue;
24572
+ const idleFor = now - state.lastAccess;
24573
+ const settled = state.status === "ready" || now - state.startedAt > this.startupTimeoutMs;
24574
+ if (settled && idleFor > idle) {
24575
+ debugLog("sites", `reaping ${host} (idle ${Math.round(idleFor / 1000)}s)`, this.verbose);
24576
+ await this.stop(host);
24577
+ }
24578
+ }
24579
+ }
24580
+ list() {
24581
+ const now = this.now();
24582
+ return [...this.sites.values()].map((s) => ({
24583
+ host: s.site.host,
24584
+ dir: s.site.dir,
24585
+ status: s.status,
24586
+ pid: s.handle?.pid ?? null,
24587
+ ports: Object.fromEntries(s.ports),
24588
+ uptimeMs: now - s.startedAt,
24589
+ idleMs: now - s.lastAccess,
24590
+ error: s.error
24591
+ }));
24592
+ }
24593
+ async stopAll() {
24594
+ this.stopped = true;
24595
+ clearInterval(this.reaper);
24596
+ await Promise.allSettled([...this.sites.keys()].map((host) => this.stop(host)));
24597
+ }
24598
+ }
24599
+ function delay(ms) {
24600
+ return new Promise((resolve14) => setTimeout(resolve14, ms));
24601
+ }
24602
+ async function defaultReadinessProbe(port) {
24603
+ const controller = new AbortController;
24604
+ const timer = setTimeout(() => controller.abort(), 2000);
24605
+ try {
24606
+ await fetch(`http://127.0.0.1:${port}/`, { signal: controller.signal, redirect: "manual" });
24607
+ return true;
24608
+ } catch {
24609
+ return false;
24610
+ } finally {
24611
+ clearTimeout(timer);
24612
+ }
24613
+ }
24614
+ function makeDefaultLauncher(verbose) {
24615
+ const drop = privilegeDrop();
24616
+ return ({ command, cwd, env: env2, logPath }) => {
24617
+ const fd = openSync6(logPath, "a");
24618
+ try {
24619
+ const child = spawn("sh", ["-c", command], {
24620
+ cwd,
24621
+ env: env2,
24622
+ detached: true,
24623
+ stdio: ["ignore", fd, fd],
24624
+ ...drop ? { uid: drop.uid, gid: drop.gid } : {}
24625
+ });
24626
+ const pid2 = child.pid;
24627
+ if (pid2 === undefined)
24628
+ throw new Error("spawn returned no pid");
24629
+ const exited = new Promise((resolve14) => {
24630
+ child.once("exit", (code) => {
24631
+ resolve14(code);
24632
+ });
24633
+ child.once("error", () => {
24634
+ resolve14(null);
24635
+ });
24636
+ }).finally(() => {
24637
+ try {
24638
+ closeSync6(fd);
24639
+ } catch {}
24640
+ });
24641
+ debugLog("sites", `spawned pid ${pid2}: sh -c ${command} (cwd ${cwd})`, verbose);
24642
+ return {
24643
+ pid: pid2,
24644
+ exited,
24645
+ stop: (signal = "SIGTERM") => {
24646
+ try {
24647
+ process25.kill(-pid2, signal);
24648
+ } catch {
24649
+ try {
24650
+ child.kill(signal);
24651
+ } catch {}
24652
+ }
24653
+ }
24654
+ };
24655
+ } catch (err) {
24656
+ try {
24657
+ closeSync6(fd);
24658
+ } catch {}
24659
+ throw err;
24660
+ }
24661
+ };
24662
+ }
24663
+ function privilegeDrop() {
24664
+ if (process25.platform === "win32")
24665
+ return null;
24666
+ const isRoot = typeof process25.getuid === "function" && process25.getuid() === 0;
24667
+ if (!isRoot)
24668
+ return null;
24669
+ const uid = Number.parseInt(process25.env.SUDO_UID ?? "", 10);
24670
+ const gid = Number.parseInt(process25.env.SUDO_GID ?? "", 10);
24671
+ if (!Number.isInteger(uid) || uid <= 0 || !Number.isInteger(gid) || gid <= 0)
24672
+ return null;
24673
+ return { uid, gid };
24674
+ }
24675
+ var DEFAULT_STARTUP_TIMEOUT_MS = 120000, DEFAULT_POLL_INTERVAL_MS = 200, DEFAULT_REAP_INTERVAL_MS = 30000, DEFAULT_RESTART_DELAY_MS = 3000, KILL_GRACE_MS = 4000;
24676
+ var init_site_supervisor = __esm(() => {
24677
+ init_host_routes();
24678
+ init_port_manager();
24679
+ init_registry();
24680
+ init_utils();
24681
+ init_logger();
24682
+ });
24683
+ // ../rpx/src/dns-state.ts
24684
+ import * as fsp5 from "node:fs/promises";
24685
+ import { homedir as homedir11 } from "node:os";
24686
+ import * as path7 from "node:path";
23953
24687
  function defaultRpxDir() {
23954
- return path6.join(homedir10(), ".stacks", "rpx");
24688
+ return path7.join(homedir11(), ".stacks", "rpx");
23955
24689
  }
23956
24690
  function getDnsStatePath(rpxDir = defaultRpxDir()) {
23957
- return path6.join(rpxDir, RPX_DNS_STATE_FILE);
24691
+ return path7.join(rpxDir, RPX_DNS_STATE_FILE);
23958
24692
  }
23959
24693
  async function loadDnsState(rpxDir = defaultRpxDir()) {
23960
24694
  try {
23961
- const raw = await fsp4.readFile(getDnsStatePath(rpxDir), "utf8");
24695
+ const raw = await fsp5.readFile(getDnsStatePath(rpxDir), "utf8");
23962
24696
  const parsed = JSON.parse(raw);
23963
24697
  if (parsed.version !== DNS_STATE_VERSION || !Array.isArray(parsed.resolvers))
23964
24698
  return null;
@@ -23976,12 +24710,12 @@ async function loadDnsState(rpxDir = defaultRpxDir()) {
23976
24710
  }
23977
24711
  }
23978
24712
  async function saveDnsState(rpxDir, state) {
23979
- await fsp4.mkdir(rpxDir, { recursive: true });
23980
- await fsp4.writeFile(getDnsStatePath(rpxDir), `${JSON.stringify(state, null, 2)}
24713
+ await fsp5.mkdir(rpxDir, { recursive: true });
24714
+ await fsp5.writeFile(getDnsStatePath(rpxDir), `${JSON.stringify(state, null, 2)}
23981
24715
  `, "utf8");
23982
24716
  }
23983
24717
  async function clearDnsState(rpxDir) {
23984
- await fsp4.rm(getDnsStatePath(rpxDir), { force: true });
24718
+ await fsp5.rm(getDnsStatePath(rpxDir), { force: true });
23985
24719
  }
23986
24720
  function normalizeDevDomain(raw) {
23987
24721
  const domain = raw.trim().toLowerCase().replace(/\.$/, "");
@@ -24057,9 +24791,9 @@ __export(exports_dns, {
24057
24791
  DNS_PORT: () => DNS_PORT
24058
24792
  });
24059
24793
  import dgram from "node:dgram";
24060
- import * as fsp5 from "node:fs/promises";
24061
- import * as path7 from "node:path";
24062
- import * as process25 from "node:process";
24794
+ import * as fsp6 from "node:fs/promises";
24795
+ import * as path8 from "node:path";
24796
+ import * as process26 from "node:process";
24063
24797
  function parseHeader(buffer) {
24064
24798
  return {
24065
24799
  id: buffer.readUInt16BE(0),
@@ -24173,7 +24907,7 @@ function buildNxdomainResponse(queryId, question) {
24173
24907
  return Buffer.concat(parts);
24174
24908
  }
24175
24909
  async function startDnsServer(domains, verbose) {
24176
- if (process25.platform !== "darwin")
24910
+ if (process26.platform !== "darwin")
24177
24911
  return false;
24178
24912
  const devDomains = devDomainsFromHosts(domains);
24179
24913
  if (devDomains.length === 0)
@@ -24293,14 +25027,14 @@ port ${DNS_PORT}
24293
25027
  `;
24294
25028
  }
24295
25029
  function resolverFilePath(basename) {
24296
- return path7.join(MACOS_RESOLVER_DIR, basename);
25030
+ return path8.join(MACOS_RESOLVER_DIR, basename);
24297
25031
  }
24298
25032
  function contentLooksLikeRpxResolver(content) {
24299
25033
  return content.includes("127.0.0.1") && content.includes(String(DNS_PORT));
24300
25034
  }
24301
25035
  async function readResolverFile(basename) {
24302
25036
  try {
24303
- return await fsp5.readFile(resolverFilePath(basename), "utf8");
25037
+ return await fsp6.readFile(resolverFilePath(basename), "utf8");
24304
25038
  } catch (err) {
24305
25039
  if (err.code === "ENOENT")
24306
25040
  return null;
@@ -24308,7 +25042,7 @@ async function readResolverFile(basename) {
24308
25042
  }
24309
25043
  }
24310
25044
  async function flushDnsCache(verbose) {
24311
- if (process25.platform !== "darwin")
25045
+ if (process26.platform !== "darwin")
24312
25046
  return;
24313
25047
  const { execSudoSync: execSudoSync2, getSudoPassword: getSudoPassword2, isProcessElevated: isProcessElevated2 } = await Promise.resolve().then(() => (init_utils(), exports_utils));
24314
25048
  if (!isProcessElevated2() && !getSudoPassword2()) {
@@ -24345,7 +25079,7 @@ async function setupResolver(verbose, domains) {
24345
25079
  return setupDevelopmentDns({ domains: domains ?? [], verbose });
24346
25080
  }
24347
25081
  async function installResolvers(basenames, verbose) {
24348
- if (process25.platform !== "darwin")
25082
+ if (process26.platform !== "darwin")
24349
25083
  return true;
24350
25084
  const { getSudoPassword: getSudoPassword2, isProcessElevated: isProcessElevated2 } = await Promise.resolve().then(() => (init_utils(), exports_utils));
24351
25085
  if (!isProcessElevated2() && !getSudoPassword2()) {
@@ -24363,7 +25097,7 @@ async function installResolvers(basenames, verbose) {
24363
25097
  }
24364
25098
  }
24365
25099
  async function uninstallResolvers(basenames, verbose) {
24366
- if (process25.platform !== "darwin")
25100
+ if (process26.platform !== "darwin")
24367
25101
  return;
24368
25102
  const { getSudoPassword: getSudoPassword2, isProcessElevated: isProcessElevated2 } = await Promise.resolve().then(() => (init_utils(), exports_utils));
24369
25103
  if (!isProcessElevated2() && !getSudoPassword2())
@@ -24377,7 +25111,7 @@ async function uninstallResolvers(basenames, verbose) {
24377
25111
  }
24378
25112
  }
24379
25113
  async function removeLegacyTldResolvers(verbose) {
24380
- if (process25.platform !== "darwin")
25114
+ if (process26.platform !== "darwin")
24381
25115
  return [];
24382
25116
  const removed = [];
24383
25117
  for (const label of LEGACY_TLD_RESOLVER_LABELS) {
@@ -24412,7 +25146,7 @@ async function setupDevelopmentDns(opts) {
24412
25146
  version: DNS_STATE_VERSION,
24413
25147
  resolvers: basenames,
24414
25148
  domains,
24415
- ownerPid: opts.ownerPid ?? process25.pid,
25149
+ ownerPid: opts.ownerPid ?? process26.pid,
24416
25150
  updatedAt: new Date().toISOString()
24417
25151
  };
24418
25152
  await saveDnsState(rpxDir, state);
@@ -24436,7 +25170,7 @@ async function syncDevelopmentDnsFromRegistry(entries, opts = {}) {
24436
25170
  domains,
24437
25171
  rpxDir,
24438
25172
  verbose: opts.verbose,
24439
- ownerPid: opts.ownerPid ?? process25.pid
25173
+ ownerPid: opts.ownerPid ?? process26.pid
24440
25174
  });
24441
25175
  }
24442
25176
  async function tearDownDevelopmentDns(opts = {}) {
@@ -24476,19 +25210,19 @@ var init_dns = __esm(() => {
24476
25210
 
24477
25211
  // ../rpx/src/daemon.ts
24478
25212
  import { spawn as nodeSpawn } from "node:child_process";
24479
- import * as fsp6 from "node:fs/promises";
24480
- import { homedir as homedir11 } from "node:os";
24481
- import * as path8 from "node:path";
24482
- import * as process26 from "node:process";
25213
+ import * as fsp7 from "node:fs/promises";
25214
+ import { homedir as homedir12 } from "node:os";
25215
+ import * as path9 from "node:path";
25216
+ import * as process27 from "node:process";
24483
25217
  function getDaemonRpxDir() {
24484
- return path8.join(homedir11(), ".stacks", "rpx");
25218
+ return path9.join(homedir12(), ".stacks", "rpx");
24485
25219
  }
24486
25220
  function getDaemonPidPath(rpxDir = getDaemonRpxDir()) {
24487
- return path8.join(rpxDir, "daemon.pid");
25221
+ return path9.join(rpxDir, "daemon.pid");
24488
25222
  }
24489
25223
  async function readDaemonPid(rpxDir = getDaemonRpxDir()) {
24490
25224
  try {
24491
- const raw = await fsp6.readFile(getDaemonPidPath(rpxDir), "utf8");
25225
+ const raw = await fsp7.readFile(getDaemonPidPath(rpxDir), "utf8");
24492
25226
  const n2 = Number.parseInt(raw.trim(), 10);
24493
25227
  if (!Number.isFinite(n2) || n2 <= 0)
24494
25228
  return null;
@@ -24500,14 +25234,14 @@ async function readDaemonPid(rpxDir = getDaemonRpxDir()) {
24500
25234
  }
24501
25235
  }
24502
25236
  async function releaseDaemonLock(rpxDir = getDaemonRpxDir()) {
24503
- await fsp6.unlink(getDaemonPidPath(rpxDir)).catch(() => {});
25237
+ await fsp7.unlink(getDaemonPidPath(rpxDir)).catch(() => {});
24504
25238
  }
24505
25239
  function defaultDaemonSpawnCommand() {
24506
- const exec = process26.execPath;
24507
- const interpName = path8.basename(exec).toLowerCase();
25240
+ const exec = process27.execPath;
25241
+ const interpName = path9.basename(exec).toLowerCase();
24508
25242
  const isInterpreter = interpName === "bun" || interpName === "node" || interpName.startsWith("bun-");
24509
- if (isInterpreter && process26.argv[1])
24510
- return [exec, process26.argv[1], "daemon:start"];
25243
+ if (isInterpreter && process27.argv[1])
25244
+ return [exec, process27.argv[1], "daemon:start"];
24511
25245
  return [exec, "daemon:start"];
24512
25246
  }
24513
25247
  async function ensureDaemonRunning(opts = {}) {
@@ -24525,7 +25259,7 @@ async function ensureDaemonRunning(opts = {}) {
24525
25259
  debugLog("daemon", `ensureDaemonRunning: clearing stale pid=${existingPid}`, verbose);
24526
25260
  await releaseDaemonLock(rpxDir);
24527
25261
  }
24528
- await fsp6.mkdir(rpxDir, { recursive: true });
25262
+ await fsp7.mkdir(rpxDir, { recursive: true });
24529
25263
  const command = opts.spawnCommand ?? defaultDaemonSpawnCommand();
24530
25264
  if (command.length === 0)
24531
25265
  throw new Error("ensureDaemonRunning: spawnCommand is empty");
@@ -24533,8 +25267,8 @@ async function ensureDaemonRunning(opts = {}) {
24533
25267
  const child = nodeSpawn(command[0], command.slice(1), {
24534
25268
  detached: true,
24535
25269
  stdio: "ignore",
24536
- cwd: opts.spawnCwd ?? process26.cwd(),
24537
- env: opts.spawnEnv ? { ...process26.env, ...opts.spawnEnv } : process26.env
25270
+ cwd: opts.spawnCwd ?? process27.cwd(),
25271
+ env: opts.spawnEnv ? { ...process27.env, ...opts.spawnEnv } : process27.env
24538
25272
  });
24539
25273
  child.unref();
24540
25274
  let spawnError = null;
@@ -24565,15 +25299,18 @@ var init_daemon = __esm(() => {
24565
25299
  init_host_routes();
24566
25300
  init_sni();
24567
25301
  init_on_demand();
25302
+ init_site_resolver();
25303
+ init_site_supervisor();
24568
25304
  init_static_files();
25305
+ init_auth();
24569
25306
  init_registry();
24570
25307
  init_dns();
24571
25308
  init_utils();
24572
25309
  });
24573
25310
 
24574
25311
  // src/index.ts
24575
- import { exec as exec3, spawn as spawn2 } from "node:child_process";
24576
- import * as process31 from "node:process";
25312
+ import { exec as exec3, spawn as spawn3 } from "node:child_process";
25313
+ import * as process34 from "node:process";
24577
25314
  import { promisify as promisify2 } from "node:util";
24578
25315
 
24579
25316
  // ../rpx/src/start.ts
@@ -24581,7 +25318,7 @@ init_logger();
24581
25318
  import { execSync as execSync4 } from "node:child_process";
24582
25319
  import * as http from "node:http";
24583
25320
  import * as net2 from "node:net";
24584
- import * as process30 from "node:process";
25321
+ import * as process31 from "node:process";
24585
25322
 
24586
25323
  // ../rpx/src/colors.ts
24587
25324
  var c = (open, close) => (str) => `\x1B[${open}m${str}\x1B[${close}m`;
@@ -24601,8 +25338,8 @@ init_logger();
24601
25338
  init_registry();
24602
25339
  init_utils();
24603
25340
  import * as fs3 from "node:fs";
24604
- import * as path9 from "node:path";
24605
- import * as process27 from "node:process";
25341
+ import * as path10 from "node:path";
25342
+ import * as process28 from "node:process";
24606
25343
  function deriveIdFromTarget(to, routePath) {
24607
25344
  const base = routePath && routePath !== "/" ? `${to}${routePath}` : to;
24608
25345
  const cleaned = base.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 128);
@@ -24630,8 +25367,8 @@ async function runViaDaemon(opts) {
24630
25367
  from: p2.from,
24631
25368
  to: p2.to,
24632
25369
  path: p2.path,
24633
- pid: opts.persistent ? undefined : process27.pid,
24634
- cwd: process27.cwd(),
25370
+ pid: opts.persistent ? undefined : process28.pid,
25371
+ cwd: process28.cwd(),
24635
25372
  createdAt,
24636
25373
  cleanUrls: p2.cleanUrls,
24637
25374
  changeOrigin: p2.changeOrigin,
@@ -24668,16 +25405,16 @@ async function runViaDaemon(opts) {
24668
25405
  };
24669
25406
  const onSignal = (sig) => {
24670
25407
  debugLog("runner", `received ${sig}, unregistering ${idsForCleanup.length} entries`, verbose);
24671
- cleanup().finally(() => process27.exit(0));
25408
+ cleanup().finally(() => process28.exit(0));
24672
25409
  };
24673
- process27.once("SIGINT", onSignal);
24674
- process27.once("SIGTERM", onSignal);
24675
- process27.once("exit", () => {
25410
+ process28.once("SIGINT", onSignal);
25411
+ process28.once("SIGTERM", onSignal);
25412
+ process28.once("exit", () => {
24676
25413
  if (cleaned)
24677
25414
  return;
24678
25415
  for (const id of idsForCleanup) {
24679
25416
  try {
24680
- fs3.unlinkSync(path9.join(dirForCleanup, `${id}.json`));
25417
+ fs3.unlinkSync(path10.join(dirForCleanup, `${id}.json`));
24681
25418
  } catch {}
24682
25419
  }
24683
25420
  });
@@ -24689,18 +25426,18 @@ init_utils();
24689
25426
  import { exec } from "node:child_process";
24690
25427
  import fs4 from "node:fs";
24691
25428
  import os2 from "node:os";
24692
- import path10 from "node:path";
24693
- import * as process28 from "node:process";
25429
+ import path11 from "node:path";
25430
+ import * as process29 from "node:process";
24694
25431
  import { promisify } from "node:util";
24695
25432
  var execAsync = promisify(exec);
24696
25433
  function isLoopbackDevelopmentHost(host) {
24697
25434
  const normalized = host.trim().toLowerCase();
24698
25435
  return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".localhost.");
24699
25436
  }
24700
- var hostsFilePath = process28.platform === "win32" ? path10.join(process28.env.windir || "C:\\Windows", "System32", "drivers", "etc", "hosts") : "/etc/hosts";
25437
+ var hostsFilePath = process29.platform === "win32" ? path11.join(process29.env.windir || "C:\\Windows", "System32", "drivers", "etc", "hosts") : "/etc/hosts";
24701
25438
  var sudoPrivilegesAcquired = false;
24702
25439
  async function execSudo(command) {
24703
- if (process28.platform === "win32")
25440
+ if (process29.platform === "win32")
24704
25441
  throw new Error("Administrator privileges required on Windows");
24705
25442
  if (isProcessElevated()) {
24706
25443
  const { stdout } = await execAsync(command);
@@ -24771,7 +25508,7 @@ async function addHosts(hosts, verbose) {
24771
25508
  127.0.0.1 ${host}
24772
25509
  ::1 ${host}`).join(`
24773
25510
  `);
24774
- const tmpFile = path10.join(os2.tmpdir(), `rpx-hosts-${Date.now()}.tmp`);
25511
+ const tmpFile = path11.join(os2.tmpdir(), `rpx-hosts-${Date.now()}.tmp`);
24775
25512
  try {
24776
25513
  await fs4.promises.writeFile(tmpFile, existingContent + hostEntries, "utf8");
24777
25514
  await execSudo(`cat "${tmpFile}" | tee "${hostsFilePath}" > /dev/null`);
@@ -24833,7 +25570,7 @@ async function removeHosts(hosts, verbose) {
24833
25570
  const newContent = `${filteredLines.join(`
24834
25571
  `)}
24835
25572
  `;
24836
- const tmpFile = path10.join(os2.tmpdir(), `rpx-hosts-${Date.now()}.tmp`);
25573
+ const tmpFile = path11.join(os2.tmpdir(), `rpx-hosts-${Date.now()}.tmp`);
24837
25574
  try {
24838
25575
  await fs4.promises.writeFile(tmpFile, newContent, "utf8");
24839
25576
  await execSudo(`cat "${tmpFile}" | tee "${hostsFilePath}" > /dev/null`);
@@ -24882,141 +25619,13 @@ async function checkHosts(hosts, verbose) {
24882
25619
 
24883
25620
  // ../rpx/src/start.ts
24884
25621
  init_https();
24885
-
24886
- // ../rpx/src/port-manager.ts
24887
- init_utils();
24888
- import * as net from "node:net";
24889
- function isPortInUse(port, hostname, verbose) {
24890
- debugLog("port", `Checking if port ${port} is in use on ${hostname}`, verbose);
24891
- return new Promise((resolve14) => {
24892
- const server = net.createServer();
24893
- const timeout = setTimeout(() => {
24894
- debugLog("port", `Checking port ${port} timed out, assuming it's in use`, verbose);
24895
- server.close();
24896
- resolve14(true);
24897
- }, 3000);
24898
- server.once("error", (err) => {
24899
- clearTimeout(timeout);
24900
- if (err.code === "EADDRINUSE") {
24901
- debugLog("port", `Port ${port} is in use`, verbose);
24902
- resolve14(true);
24903
- } else {
24904
- debugLog("port", `Error checking port ${port}: ${err.message}`, verbose);
24905
- resolve14(true);
24906
- }
24907
- });
24908
- server.once("listening", () => {
24909
- clearTimeout(timeout);
24910
- debugLog("port", `Port ${port} is available`, verbose);
24911
- server.close();
24912
- resolve14(false);
24913
- });
24914
- try {
24915
- server.listen(port, hostname);
24916
- } catch (err) {
24917
- clearTimeout(timeout);
24918
- debugLog("port", `Exception checking port ${port}: ${err}`, verbose);
24919
- resolve14(true);
24920
- }
24921
- });
24922
- }
24923
- async function findAvailablePort(startPort, hostname, verbose, maxAttempts = 50) {
24924
- debugLog("port", `Finding available port starting from ${startPort} (max attempts: ${maxAttempts})`, verbose);
24925
- let port = startPort;
24926
- let attempts = 0;
24927
- while (attempts < maxAttempts) {
24928
- attempts++;
24929
- const isInUse = await isPortInUse(port, hostname, verbose);
24930
- if (!isInUse) {
24931
- debugLog("port", `Found available port: ${port} after ${attempts} attempts`, verbose);
24932
- return port;
24933
- }
24934
- debugLog("port", `Port ${port} is in use, trying ${port + 1} (attempt ${attempts}/${maxAttempts})`, verbose);
24935
- port++;
24936
- }
24937
- throw new Error(`Unable to find available port after ${maxAttempts} attempts starting from ${startPort}`);
24938
- }
24939
- function testPortConnectivity(port, hostname, timeout = 5000, verbose) {
24940
- debugLog("port", `Testing connection to ${hostname}:${port}`, verbose);
24941
- return new Promise((resolve14) => {
24942
- const socket = net.connect({
24943
- host: hostname,
24944
- port,
24945
- timeout
24946
- });
24947
- socket.once("connect", () => {
24948
- debugLog("port", `Successfully connected to ${hostname}:${port}`, verbose);
24949
- socket.end();
24950
- resolve14(true);
24951
- });
24952
- socket.once("timeout", () => {
24953
- debugLog("port", `Connection to ${hostname}:${port} timed out`, verbose);
24954
- socket.destroy();
24955
- resolve14(false);
24956
- });
24957
- socket.once("error", (err) => {
24958
- debugLog("port", `Failed to connect to ${hostname}:${port}: ${err.message}`, verbose);
24959
- socket.destroy();
24960
- resolve14(false);
24961
- });
24962
- });
24963
- }
24964
-
24965
- class DefaultPortManager {
24966
- usedPorts = new Set;
24967
- hostname;
24968
- verbose;
24969
- maxRetries;
24970
- constructor(hostname = "0.0.0.0", verbose, maxRetries = 50) {
24971
- this.hostname = hostname;
24972
- this.verbose = verbose;
24973
- this.maxRetries = maxRetries;
24974
- }
24975
- async getNextAvailablePort(startPort, testConnectivity = false) {
24976
- if (this.usedPorts.has(startPort)) {
24977
- return this.findNextAvailablePort(startPort + 1, testConnectivity);
24978
- }
24979
- const isInUse = await isPortInUse(startPort, this.hostname, this.verbose);
24980
- if (isInUse) {
24981
- return this.findNextAvailablePort(startPort + 1, testConnectivity);
24982
- }
24983
- if (testConnectivity) {
24984
- const isConnectable = await testPortConnectivity(startPort, this.hostname, 3000, this.verbose);
24985
- if (!isConnectable) {
24986
- debugLog("port", `Port ${startPort} is available but not connectable, trying next port`, this.verbose);
24987
- return this.findNextAvailablePort(startPort + 1, testConnectivity);
24988
- }
24989
- }
24990
- this.usedPorts.add(startPort);
24991
- return startPort;
24992
- }
24993
- async findNextAvailablePort(startPort, testConnectivity = false) {
24994
- const port = await findAvailablePort(startPort, this.hostname, this.verbose, this.maxRetries);
24995
- if (testConnectivity) {
24996
- const isConnectable = await testPortConnectivity(port, this.hostname, 3000, this.verbose);
24997
- if (!isConnectable) {
24998
- if (port < startPort + this.maxRetries) {
24999
- return this.findNextAvailablePort(port + 1, testConnectivity);
25000
- } else {
25001
- throw new Error(`Unable to find a connectable port after ${this.maxRetries} attempts`);
25002
- }
25003
- }
25004
- }
25005
- this.usedPorts.add(port);
25006
- return port;
25007
- }
25008
- releasePort(port) {
25009
- debugLog("port", `Releasing port ${port}`, this.verbose);
25010
- this.usedPorts.delete(port);
25011
- }
25012
- }
25013
- var portManager = new DefaultPortManager;
25622
+ init_port_manager();
25014
25623
 
25015
25624
  // ../rpx/src/process-manager.ts
25016
25625
  init_logger();
25017
25626
  init_utils();
25018
- import { spawn } from "node:child_process";
25019
- import * as process29 from "node:process";
25627
+ import { spawn as spawn2 } from "node:child_process";
25628
+ import * as process30 from "node:process";
25020
25629
 
25021
25630
  class ProcessManager {
25022
25631
  processes = new Map;
@@ -25027,14 +25636,14 @@ class ProcessManager {
25027
25636
  return;
25028
25637
  }
25029
25638
  const [cmd, ...args] = options.command.split(" ");
25030
- const cwd4 = options.cwd || process29.cwd();
25639
+ const cwd4 = options.cwd || process30.cwd();
25031
25640
  debugLog("start", `Starting process ${id}:`, verbose);
25032
25641
  debugLog("start", ` Command: ${cmd} ${args.join(" ")}`, verbose);
25033
25642
  debugLog("start", ` Working directory: ${cwd4}`, verbose);
25034
25643
  debugLog("start", ` Environment variables: ${safeStringify(options.env)}`, verbose);
25035
- const childProcess = spawn(cmd, args, {
25644
+ const childProcess = spawn2(cmd, args, {
25036
25645
  cwd: cwd4,
25037
- env: { ...process29.env, ...options.env },
25646
+ env: { ...process30.env, ...options.env },
25038
25647
  shell: true,
25039
25648
  stdio: "inherit"
25040
25649
  });
@@ -25134,7 +25743,7 @@ class ProcessManager {
25134
25743
  var processManager = new ProcessManager;
25135
25744
 
25136
25745
  // ../rpx/src/origin-guard.ts
25137
- import { timingSafeEqual } from "node:crypto";
25746
+ import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
25138
25747
  function secretMatches(provided, expected) {
25139
25748
  if (provided == null)
25140
25749
  return false;
@@ -25142,7 +25751,7 @@ function secretMatches(provided, expected) {
25142
25751
  const b2 = Buffer.from(expected);
25143
25752
  if (a2.length !== b2.length)
25144
25753
  return false;
25145
- return timingSafeEqual(a2, b2);
25754
+ return timingSafeEqual2(a2, b2);
25146
25755
  }
25147
25756
  function normalizeHost(host) {
25148
25757
  return host.toLowerCase().replace(/\.$/, "");
@@ -25196,6 +25805,26 @@ function createOriginGuard(options) {
25196
25805
 
25197
25806
  // ../rpx/src/start.ts
25198
25807
  init_proxy_handler();
25808
+
25809
+ // ../rpx/src/acme-challenge.ts
25810
+ import * as fs5 from "node:fs";
25811
+ import * as path12 from "node:path";
25812
+ var ACME_CHALLENGE_PREFIX2 = "/.well-known/acme-challenge/";
25813
+ function readAcmeChallenge(webroot, pathname) {
25814
+ if (!webroot || !pathname.startsWith(ACME_CHALLENGE_PREFIX2))
25815
+ return null;
25816
+ const token = pathname.slice(ACME_CHALLENGE_PREFIX2.length);
25817
+ if (!token || !/^[A-Za-z0-9_-]+$/.test(token))
25818
+ return null;
25819
+ try {
25820
+ return fs5.readFileSync(path12.join(webroot, token), "utf8");
25821
+ } catch {
25822
+ return null;
25823
+ }
25824
+ }
25825
+
25826
+ // ../rpx/src/start.ts
25827
+ init_auth();
25199
25828
  init_host_routes();
25200
25829
  init_sni();
25201
25830
  init_static_files();
@@ -25288,8 +25917,8 @@ async function cleanup(options) {
25288
25917
  cleanupPromiseResolve = null;
25289
25918
  isCleaningUp = false;
25290
25919
  const isVitePluginCall = options && "vitePluginUsage" in options && options.vitePluginUsage === true;
25291
- if (process30.env.NODE_ENV !== "test" && process30.env.BUN_ENV !== "test" && !isVitePluginCall) {
25292
- process30.exit(0);
25920
+ if (process31.env.NODE_ENV !== "test" && process31.env.BUN_ENV !== "test" && !isVitePluginCall) {
25921
+ process31.exit(0);
25293
25922
  }
25294
25923
  }
25295
25924
  return cleanupPromise;
@@ -25298,31 +25927,31 @@ var isHandlingSignal = false;
25298
25927
  function signalHandler(signal) {
25299
25928
  if (isHandlingSignal) {
25300
25929
  debugLog("signal", `Received second ${signal} signal, forcing exit`, true);
25301
- process30.exit(1);
25930
+ process31.exit(1);
25302
25931
  return;
25303
25932
  }
25304
25933
  isHandlingSignal = true;
25305
25934
  debugLog("signal", `Received ${signal} signal, initiating cleanup`, true);
25306
25935
  cleanup().catch((err) => {
25307
25936
  debugLog("signal", `Cleanup failed after ${signal}: ${err}`, true);
25308
- process30.exit(1);
25937
+ process31.exit(1);
25309
25938
  }).finally(() => {
25310
25939
  isHandlingSignal = false;
25311
25940
  });
25312
25941
  }
25313
- process30.once("SIGINT", () => signalHandler("SIGINT"));
25314
- process30.once("SIGTERM", () => signalHandler("SIGTERM"));
25315
- process30.on("uncaughtException", (err) => {
25942
+ process31.once("SIGINT", () => signalHandler("SIGINT"));
25943
+ process31.once("SIGTERM", () => signalHandler("SIGTERM"));
25944
+ process31.on("uncaughtException", (err) => {
25316
25945
  log.error("Uncaught exception (continuing):", err);
25317
25946
  });
25318
- process30.on("unhandledRejection", (reason) => {
25947
+ process31.on("unhandledRejection", (reason) => {
25319
25948
  log.error("Unhandled rejection (continuing):", reason);
25320
25949
  });
25321
25950
  async function testConnection(hostname, port, verbose, retries = 5) {
25322
25951
  debugLog("connection", `Testing connection to ${hostname}:${port} (retries left: ${retries})`, verbose);
25323
25952
  const maxTestDuration = 15000;
25324
25953
  const startTime = Date.now();
25325
- if (process30.env.RPX_BYPASS_CONNECTION_TEST === "true") {
25954
+ if (process31.env.RPX_BYPASS_CONNECTION_TEST === "true") {
25326
25955
  debugLog("connection", `Bypassing connection test for ${hostname}:${port} due to RPX_BYPASS_CONNECTION_TEST flag`, verbose);
25327
25956
  return;
25328
25957
  }
@@ -25418,7 +26047,7 @@ async function startServer(options) {
25418
26047
  log.warn("You can manually add this entry to your hosts file:");
25419
26048
  log.warn(`127.0.0.1 ${toUrl.hostname}`);
25420
26049
  log.warn(`::1 ${toUrl.hostname}`);
25421
- if (process30.platform === "win32") {
26050
+ if (process31.platform === "win32") {
25422
26051
  log.warn("On Windows:");
25423
26052
  log.warn("1. Run notepad as administrator");
25424
26053
  log.warn("2. Open C:\\Windows\\System32\\drivers\\etc\\hosts");
@@ -25491,7 +26120,7 @@ async function startServer(options) {
25491
26120
  ssl: sslConfig
25492
26121
  });
25493
26122
  }
25494
- async function createProxyServer(from, to, listenPort, sourceUrl, ssl, vitePluginUsage, verbose, cleanUrls, changeOrigin) {
26123
+ async function createProxyServer(from, to, listenPort, sourceUrl, ssl, vitePluginUsage, verbose, cleanUrls, changeOrigin, auth) {
25495
26124
  debugLog("proxy", `Creating proxy server ${from} -> ${to} with cleanUrls: ${cleanUrls}`, verbose);
25496
26125
  const routeEntries = [{
25497
26126
  host: to,
@@ -25499,7 +26128,8 @@ async function createProxyServer(from, to, listenPort, sourceUrl, ssl, vitePlugi
25499
26128
  sourceHost: sourceUrl.host,
25500
26129
  cleanUrls: cleanUrls || false,
25501
26130
  changeOrigin: changeOrigin || false,
25502
- basePath: "/"
26131
+ basePath: "/",
26132
+ auth
25503
26133
  }
25504
26134
  }];
25505
26135
  const server = createSharedProxyServer({ routeEntries, listenPort, sslConfig: ssl, originGuard: null, verbose: verbose ?? false });
@@ -25537,7 +26167,7 @@ async function setupProxy(options) {
25537
26167
  }
25538
26168
  }
25539
26169
  } else {
25540
- if (hostsEnabled && process30.platform !== "darwin" && to && to.includes("localhost") && !to.match(/^(localhost|127\.0\.0\.1)$/)) {
26170
+ if (hostsEnabled && process31.platform !== "darwin" && to && to.includes("localhost") && !to.match(/^(localhost|127\.0\.0\.1)$/)) {
25541
26171
  const hostsExist = await checkHosts([to], verbose);
25542
26172
  if (!hostsExist[0]) {
25543
26173
  debugLog("hosts", `${to} not found in hosts file, adding...`, verbose);
@@ -25584,7 +26214,7 @@ async function setupProxy(options) {
25584
26214
  portManager2.usedPorts.add(finalPort);
25585
26215
  debugLog("setup", `Using standard ${targetPort === 443 ? "HTTPS" : "HTTP"} port ${targetPort} for ${to}`, verbose);
25586
26216
  }
25587
- await createProxyServer(from, to, finalPort, sourceUrl, ssl, vitePluginUsage, verbose, cleanUrls, changeOrigin);
26217
+ await createProxyServer(from, to, finalPort, sourceUrl, ssl, vitePluginUsage, verbose, cleanUrls, changeOrigin, resolveAuth(options.auth));
25588
26218
  } catch (err) {
25589
26219
  debugLog("setup", `Setup failed: ${err}`, verbose);
25590
26220
  log.error(`Failed to setup reverse proxy: ${err.message}`);
@@ -25597,9 +26227,19 @@ async function setupProxy(options) {
25597
26227
  });
25598
26228
  }
25599
26229
  }
25600
- function startHttpRedirectServer(verbose, httpPort = 80, httpsPort = 443) {
26230
+ function startHttpRedirectServer(verbose, httpPort = 80, httpsPort = 443, acmeChallengeWebroot) {
25601
26231
  debugLog("redirect", `Starting HTTP redirect server on port ${httpPort}`, verbose);
25602
26232
  const server = http.createServer((req, res) => {
26233
+ if (acmeChallengeWebroot && req.url) {
26234
+ const pathname = req.url.split("?", 1)[0];
26235
+ const keyAuth = readAcmeChallenge(acmeChallengeWebroot, pathname);
26236
+ if (keyAuth != null) {
26237
+ debugLog("redirect", `Serving ACME challenge ${pathname}`, verbose);
26238
+ res.writeHead(200, { "content-type": "text/plain" });
26239
+ res.end(keyAuth);
26240
+ return;
26241
+ }
26242
+ }
25603
26243
  const rawHost = req.headers.host || "";
25604
26244
  const hostname = rawHost.includes(":") ? rawHost.slice(0, rawHost.indexOf(":")) : rawHost;
25605
26245
  const target = httpsPort === 443 ? hostname : `${hostname}:${httpsPort}`;
@@ -25726,7 +26366,7 @@ async function startProxies(options) {
25726
26366
  debugLog("watch", "No start command found in config", verbose);
25727
26367
  }
25728
26368
  const primaryDomain = "proxies" in mergedOptions && Array.isArray(mergedOptions.proxies) ? mergedOptions.proxies[0]?.to : ("to" in mergedOptions) ? mergedOptions.to : "rpx.localhost";
25729
- if (process30.platform !== "win32" && (mergedOptions.https || hostsEnabled)) {
26369
+ if (process31.platform !== "win32" && (mergedOptions.https || hostsEnabled)) {
25730
26370
  const sudoPassword = getSudoPassword();
25731
26371
  if (!sudoPassword) {
25732
26372
  try {
@@ -25776,6 +26416,7 @@ async function startProxies(options) {
25776
26416
  vitePluginUsage: mergedOptions.vitePluginUsage,
25777
26417
  start: "start" in mergedOptions ? mergedOptions.start : undefined,
25778
26418
  changeOrigin: mergedOptions.changeOrigin,
26419
+ auth: "auth" in mergedOptions ? mergedOptions.auth : undefined,
25779
26420
  verbose,
25780
26421
  _cachedSSLConfig: mergedOptions._cachedSSLConfig
25781
26422
  }];
@@ -25791,7 +26432,7 @@ async function startProxies(options) {
25791
26432
  log.info(` These TLDs have HSTS preloading which can bypass local DNS`);
25792
26433
  log.info(` Consider using reserved TLDs: .test, .localhost, or .local`);
25793
26434
  }
25794
- if (hostsEnabled && process30.platform === "darwin" && customDomains.length > 0) {
26435
+ if (hostsEnabled && process31.platform === "darwin" && customDomains.length > 0) {
25795
26436
  const { setupDevelopmentDns: setupDevelopmentDns2 } = await Promise.resolve().then(() => (init_dns(), exports_dns));
25796
26437
  const dnsStarted = await setupDevelopmentDns2({ domains: customDomains, verbose });
25797
26438
  if (dnsStarted) {
@@ -25827,8 +26468,8 @@ async function startProxies(options) {
25827
26468
  verbose: mergedOptions.verbose || false
25828
26469
  });
25829
26470
  };
25830
- process30.on("SIGINT", cleanupHandler);
25831
- process30.on("SIGTERM", cleanupHandler);
26471
+ process31.on("SIGINT", cleanupHandler);
26472
+ process31.on("SIGTERM", cleanupHandler);
25832
26473
  const singlePortMode = mergedOptions.singlePortMode === true;
25833
26474
  const httpsPort = mergedOptions.httpsPort ?? 443;
25834
26475
  const httpPort = mergedOptions.httpPort ?? 80;
@@ -25840,7 +26481,7 @@ async function startProxies(options) {
25840
26481
  const routeEntries = await collectRouteEntries(proxyOptions, hostsEnabled, verbose);
25841
26482
  const isHttpPortBusy = await isPortInUse(httpPort, "0.0.0.0", verbose);
25842
26483
  if (!isHttpPortBusy) {
25843
- startHttpRedirectServer(verbose, httpPort, httpsPort);
26484
+ startHttpRedirectServer(verbose, httpPort, httpsPort, mergedOptions.acmeChallengeWebroot);
25844
26485
  }
25845
26486
  const isPortBusy = await isPortInUse(httpsPort, "0.0.0.0", verbose);
25846
26487
  if (isPortBusy) {
@@ -25900,13 +26541,22 @@ async function collectRouteEntries(proxyOptions, hostsEnabled, verbose) {
25900
26541
  const cleanUrls = option.cleanUrls || false;
25901
26542
  const routePath = option.path;
25902
26543
  const basePath = normalizePathPrefix(routePath);
25903
- if (option.static) {
26544
+ const auth = resolveAuth(option.auth);
26545
+ if (option.redirect) {
26546
+ const redirect = resolveRedirect(option.redirect);
26547
+ routeEntries.push({
26548
+ host: domain,
26549
+ path: routePath,
26550
+ route: { redirect, basePath, auth }
26551
+ });
26552
+ debugLog("proxies", `Route: ${domain}${routePath ?? ""} → redirect ${redirect.status} ${redirect.to}${auth ? " (auth)" : ""}`, verbose);
26553
+ } else if (option.static) {
25904
26554
  routeEntries.push({
25905
26555
  host: domain,
25906
26556
  path: routePath,
25907
- route: { static: resolveStaticRoute(option.static, cleanUrls), cleanUrls, basePath }
26557
+ route: { static: resolveStaticRoute(option.static, cleanUrls), cleanUrls, basePath, auth }
25908
26558
  });
25909
- debugLog("proxies", `Route: ${domain}${routePath ?? ""} → static ${typeof option.static === "string" ? option.static : option.static.dir}`, verbose);
26559
+ debugLog("proxies", `Route: ${domain}${routePath ?? ""} → static ${typeof option.static === "string" ? option.static : option.static.dir}${auth ? " (auth)" : ""}`, verbose);
25910
26560
  } else {
25911
26561
  const fromUrl = new URL(option.from?.startsWith("http") ? option.from : `http://${option.from}`);
25912
26562
  routeEntries.push({
@@ -25917,10 +26567,11 @@ async function collectRouteEntries(proxyOptions, hostsEnabled, verbose) {
25917
26567
  cleanUrls,
25918
26568
  changeOrigin: option.changeOrigin || false,
25919
26569
  pathRewrites: option.pathRewrites,
25920
- basePath
26570
+ basePath,
26571
+ auth
25921
26572
  }
25922
26573
  });
25923
- debugLog("proxies", `Route: ${domain}${routePath ?? ""} → ${fromUrl.host}`, verbose);
26574
+ debugLog("proxies", `Route: ${domain}${routePath ?? ""} → ${fromUrl.host}${auth ? " (auth)" : ""}`, verbose);
25924
26575
  }
25925
26576
  if (seenDomains.has(domain)) {
25926
26577
  continue;
@@ -25998,15 +26649,19 @@ init_config();
25998
26649
  init_https();
25999
26650
  init_macos_trust();
26000
26651
  init_cert_inspect();
26652
+ init_port_manager();
26001
26653
  init_registry();
26002
26654
  init_dns();
26003
26655
  init_dns_state();
26004
26656
  init_daemon();
26005
26657
  init_proxy_handler();
26658
+ init_auth();
26006
26659
  init_host_routes();
26007
26660
  init_static_files();
26008
26661
  init_sni();
26009
26662
  init_on_demand();
26663
+ init_site_resolver();
26664
+ init_site_supervisor();
26010
26665
  init_utils();
26011
26666
 
26012
26667
  // src/simplified-plugin.ts
@@ -26167,11 +26822,11 @@ function VitePluginRpx(options) {
26167
26822
  if (!hasSudoAccess) {
26168
26823
  const origLog = console.log;
26169
26824
  console.log = () => {};
26170
- process31.stdout.write(`
26825
+ process34.stdout.write(`
26171
26826
  Sudo access required for proxy setup.
26172
26827
  `);
26173
26828
  const gotSudoAccess = await new Promise((resolve14) => {
26174
- const sudo = spawn2("sudo", ["true"], {
26829
+ const sudo = spawn3("sudo", ["true"], {
26175
26830
  stdio: "inherit"
26176
26831
  });
26177
26832
  sudo.on("exit", (code) => {
@@ -26181,7 +26836,7 @@ Sudo access required for proxy setup.
26181
26836
  console.log = origLog;
26182
26837
  if (!gotSudoAccess) {
26183
26838
  console.error("Failed to get sudo access. Please try again.");
26184
- process31.exit(1);
26839
+ process34.exit(1);
26185
26840
  }
26186
26841
  }
26187
26842
  }
@@ -26191,10 +26846,10 @@ Sudo access required for proxy setup.
26191
26846
  handleSignal("CLOSE").catch(console.error);
26192
26847
  });
26193
26848
  }
26194
- const registeredEvents = process31.listeners("SIGINT").length > 0 && process31.listeners("SIGTERM").length > 0;
26849
+ const registeredEvents = process34.listeners("SIGINT").length > 0 && process34.listeners("SIGTERM").length > 0;
26195
26850
  if (!registeredEvents) {
26196
- process31.once("SIGINT", () => handleSignal("SIGINT").catch(console.error));
26197
- process31.once("SIGTERM", () => handleSignal("SIGTERM").catch(console.error));
26851
+ process34.once("SIGINT", () => handleSignal("SIGINT").catch(console.error));
26852
+ process34.once("SIGTERM", () => handleSignal("SIGTERM").catch(console.error));
26198
26853
  }
26199
26854
  const colorUrl = (url) => colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`));
26200
26855
  const originalPrintUrls = serverInstance.printUrls;
@@ -26232,7 +26887,7 @@ Sudo access required for proxy setup.
26232
26887
  debug("Proxy setup complete");
26233
26888
  } catch (error) {
26234
26889
  console.error("Failed to start reverse proxy:", error);
26235
- process31.exit(1);
26890
+ process34.exit(1);
26236
26891
  }
26237
26892
  };
26238
26893
  if (serverInstance.httpServer) {