UNPKG

@stacksjs/rpx

Version:

A modern and smart reverse proxy.

244 lines (243 loc) 8.66 kB
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[] }> }