@stacksjs/rpx
Version:
A modern and smart reverse proxy.
244 lines (243 loc) • 8.66 kB
TypeScript
import type { OriginGuardOptions } from './origin-guard';
import type { RedirectRouteConfig } from './redirect';
import type { TlsConfig, TlsOption } from '@stacksjs/tlsx';
export type { TlsConfig, TlsOption };
export declare interface StartOptions {
command: string
cwd?: string
env?: Record<string, string>
}
export declare interface PathRewrite {
from: string
to: string
stripPrefix?: boolean
}
export declare interface StaticRouteConfig {
dir: string
spa?: boolean
pathRewriteStyle?: PathRewriteStyle
maxAge?: number
}
/**
* HTTP Basic auth for a single route. When set, rpx challenges every request to
* the route with `401`/`WWW-Authenticate` until valid credentials are supplied
* (proxy, static, and WebSocket transports are all gated). Credentials may be
* given inline, as a `users[]` list, and/or via an Apache `htpasswd` file.
*/
export declare interface BasicAuthConfig {
realm?: string
username?: string
password?: string
users?: Array<{ username: string, password: string }>
htpasswdFile?: string
}
/**
* One upstream in a load-balanced pool, with an optional relative weight.
*/
export declare interface UpstreamTarget {
url: string
weight?: number
}
/**
* Health checking for a load-balanced pool. Passive checks (derived from live
* traffic outcomes) are always on; active checks (periodic out-of-band probes)
* are opt-in via {@link enabled}.
*/
export declare interface HealthCheckConfig {
enabled?: boolean
path?: string
interval?: number
timeout?: number
healthyThreshold?: number
unhealthyThreshold?: number
}
/** Per-route load-balancing configuration. */
export declare interface LoadBalancerConfig {
strategy?: LoadBalancerStrategy
healthCheck?: HealthCheckConfig
}
export declare interface BaseProxyConfig {
from?: ProxyFrom
to: string
loadBalancer?: LoadBalancerConfig
auth?: BasicAuthConfig
path?: string
start?: StartOptions
pathRewrites?: PathRewrite[]
static?: string | StaticRouteConfig
redirect?: string | RedirectRouteConfig
id?: string
}
export declare interface CleanupConfig {
domains: string[]
hosts: boolean
certs: boolean
verbose: boolean
vitePluginUsage?: boolean
}
/**
* A real PEM cert+key pair on disk for one SNI server name.
*/
export declare interface DomainCert {
certPath: string
keyPath: string
}
/**
* Production TLS using real certs (e.g. Let's Encrypt) served per-domain via
* SNI on a single listener. Provide either an explicit `domains` map or a
* `certsDir` convention.
*/
export declare interface ProductionTlsConfig {
domains?: Record<string, DomainCert>
certsDir?: string
}
/**
* On-demand TLS: issue a real (Let's Encrypt, http-01) certificate for an
* unknown host the first time it's needed, gated by an `ask` callback and/or an
* `allowedSuffixes` allowlist to prevent abuse.
*
* ## Why this is "ask-gated issuance + listener recreate", not at-handshake
*
* Bun (verified on 1.3.14 + 1.4.0) has **no working SNICallback** and
* `server.reload({ tls })` does **not** update certs at runtime. So rpx cannot
* mint a cert during the TLS handshake the way Caddy's on-demand TLS does.
* Instead rpx:
* 1. Sees the first plaintext request for the host on its `:80` listener.
* 2. Asks `ask(host)` / checks `allowedSuffixes`; if approved, drives the
* ACME http-01 flow (serving the challenge from its own `:80`).
* 3. Writes the PEMs into `certsDir` and rebuilds the `:443` listener with the
* augmented SNI cert set (a sub-second `server.stop()` + re-`Bun.serve`).
* The subsequent HTTPS request then finds the freshly-issued cert.
*
* Issuance can also be triggered programmatically via the manager's
* `ensureCert(host)` (e.g. a tunnel server pre-warming a subdomain's cert on
* registration) so the cert exists before the first browser hit.
*/
export declare interface OnDemandTlsConfig {
enabled?: boolean
ask?: (host: string) => boolean | Promise<boolean>
allowedSuffixes?: string[]
email?: string
staging?: boolean
certsDir?: string
}
/**
* One backend a site exposes, mapped to a request path. rpx picks a free port,
* exports it to the dev command via {@link portEnv}, and proxies {@link path}
* (under the site host) to `localhost:<port>`.
*
* A Stacks app, for example, has three: the frontend at `/` (env `PORT`), the
* API at `/api` (env `PORT_API`), and the docs at `/docs` (env `PORT_DOCS`).
*/
export declare interface SiteRouteTemplate {
path?: string
portEnv: string
defaultPort?: number
stripPrefix?: boolean
readyGate?: boolean
}
/**
* A lazily-booted project. The first request to {@link to} starts {@link command}
* in {@link dir}; rpx holds the request behind a "starting…" splash until the
* site's ready gate passes, then proxies it. After {@link idleTimeoutMs} with no
* traffic the process is stopped again — so a machine can "have" dozens of sites
* but only run the ones in active use.
*/
export declare interface SiteConfig {
to: string
dir: string
command: string
env?: Record<string, string>
routes?: SiteRouteTemplate[]
selfRegisters?: boolean
idleTimeoutMs?: number
}
/**
* On-demand sites: lazily boot a project's dev server the first time its host is
* visited, then proxy to it (Valet/puma-dev style). Opt-in via {@link enabled}.
*/
export declare interface OnDemandSitesConfig {
enabled?: boolean
sites?: SiteConfig[]
roots?: string[]
tlds?: string[]
idleTimeoutMs?: number
startupTimeoutMs?: number
}
export declare interface SharedProxyConfig {
https: boolean | TlsOption
cleanup: boolean | CleanupOptions
vitePluginUsage: boolean
verbose: boolean
_cachedSSLConfig?: SSLConfig | null
start?: StartOptions
cleanUrls: boolean
changeOrigin?: boolean
regenerateUntrustedCerts?: boolean
singlePortMode?: boolean
httpPort?: number
httpsPort?: number
acmeChallengeWebroot?: string
viaDaemon?: boolean
hostsManagement?: boolean
productionCerts?: ProductionTlsConfig
onDemandTls?: OnDemandTlsConfig
onDemand?: OnDemandSitesConfig
originGuard?: OriginGuardOptions
}
export declare interface SingleProxyConfig extends BaseProxyConfig, SharedProxyConfig {}
export declare interface MultiProxyConfig extends SharedProxyConfig {
proxies: Array<BaseProxyConfig & { cleanUrls: boolean, pathRewrites?: PathRewrite[] }>
}
export declare interface SSLConfig {
key: string
cert: string
ca?: string | string[]
}
export declare interface ProxySetupOptions extends Omit<ProxyOption, 'from'> {
fromPort: number
sourceUrl: Pick<URL, 'hostname' | 'host'>
ssl: SSLConfig | null
from: string
originalFrom?: ProxyFrom
to: string
portManager?: PortManager
}
export declare interface PortManager {
usedPorts: Set<number>
getNextAvailablePort: (startPort: number) => Promise<number>
}
/**
* How a static-file route maps request paths to files on disk.
*
* - `directory` (default): `/about` → `<root>/about/index.html` (SSG dir style).
* - `flat`: `/about` → `<root>/about.html` (flat-file style).
*/
export type PathRewriteStyle = 'directory' | 'flat';
/**
* Upstream `host:port` to forward to. Either a single string (backward
* compatible, degenerate one-item pool) or an array of upstreams — either
* plain `host:port` strings or {@link UpstreamTarget} objects with a weight —
* to load-balance across.
*/
export type ProxyFrom = string | Array<string | UpstreamTarget>;
/** How {@link selectUpstream} picks the next upstream from a healthy pool. */
export type LoadBalancerStrategy = 'round-robin' | 'weighted-round-robin' | 'least-connections';
export type BaseProxyOptions = Partial<BaseProxyConfig>;
export type CleanupOptions = Partial<CleanupConfig>;
export type SharedProxyOptions = Partial<SharedProxyConfig>;
export type ProxyConfig = SingleProxyConfig;
export type ProxyConfigs = SingleProxyConfig | MultiProxyConfig;
export type BaseProxyOption = Partial<BaseProxyConfig>;
export type ProxyOption = Partial<SingleProxyConfig>;
export type ProxyOptions = Partial<SingleProxyConfig> | Partial<MultiProxyConfig>;
/**
* Internal shape used by `startProxies` after merging the built-in defaults with
* the caller's single- or multi-proxy options. Every field is optional, and the
* `proxies` array elements tolerate the per-proxy `cleanUrls`/`changeOrigin`
* overrides the runtime reads — so the merged object can be accessed across both
* single and multi shapes without falling back to `any`.
*/
export type ResolvedProxyOptions = Partial<SingleProxyConfig> & {
proxies?: Array<BaseProxyConfig & { cleanUrls?: boolean, changeOrigin?: boolean, pathRewrites?: PathRewrite[] }>
}