/** * Copyright (c) 2026 AzerothJS. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import type { ServerResponse } from 'node:http'; import { type Server } from 'node:http'; import { type Http2Server, type Http2ServerResponse } from 'node:http2'; import { type AnyIncoming, type ForwardedTrust } from './adapter-request.ts'; import type { WebHandler } from './edge.ts'; type AnyOutgoing = ServerResponse | Http2ServerResponse; /** * Declares the proxy in front of this server. OFF by default: `X-Forwarded-*` is * client-controlled, and believing it without a proxy lets any direct caller forge its * scheme and host - the same explicit trust boundary `clientIp` draws. * * Each header is read by how it ACCUMULATES, not by one knob: `X-Forwarded-For` is * appended per hop, so `trustedHops` counts from the right; `X-Forwarded-Proto` and * `X-Forwarded-Host` are REPLACED by every mainstream proxy, so the last written value is * the trusted one and no hop count applies. */ export type TrustProxyOptions = boolean | ForwardedTrust; /** * Converts a Node request into a web-standard Request - a LAZY one (see adapter-request.ts): * headers, disconnect signal, and body each materialize on first access, so the hot path * pays for nothing a handler does not touch. The scheme is the caller's statement about * what this socket actually speaks - the adapter cannot guess a TLS terminator's presence. * Behind a DECLARED terminating proxy, `trustProxy` lets `X-Forwarded-Proto`/`-Host` * override scheme and authority, so `context.url` tells the client's truth. */ export declare function toWebRequest(req: AnyIncoming, options?: { scheme?: 'http' | 'https'; trustProxy?: TrustProxyOptions; }): Request; export declare function writeResponse(res: AnyOutgoing, response: Response): Promise; /** * The served-socket handle that `serve`/`serveH2c` return. Generic over the concrete server * class so a caller that needs the real HTTP/1 `Server` (for ws attachment or socket tuning) * keeps its exact type: `serve` yields `Served`, `serveH2c` yields `Served`, * and the default union keeps bare annotations working. */ export interface Served { /** The listening Node server (for address(), unref(), test hooks). */ server: S; /** The bound port (resolved even when 0 was requested). */ port: number; /** * Graceful shutdown. On HTTP/1 (`serve`), `gracePeriodMs` (default 10s) bounds the ENTIRE * drain: stop accepting, wait for in-flight responses up to the deadline, then destroy * whatever remains - including upgraded sockets (WebSockets), which are cut immediately * once in-flight HTTP work has drained, because the adapter cannot see their traffic. A * WebSocket exchange mid-flight at shutdown is therefore lost (close code 1006); an app * that wants clean closes sends `close(1001)` per socket BEFORE calling this. On h2c * (`serveH2c`) the bound does NOT apply: the drain waits for open streams and sessions to * finish, as it always has. Resolves when the server is fully closed. */ shutdown(options?: { gracePeriodMs?: number | undefined; }): Promise; } /** * A connect-style middleware: handles the raw Node request itself or calls `next()` to pass * it on. This is the ONE non-web-standard extension point, and it exists for exactly one * ecosystem: dev tooling (Vite's middleware mode, and anything else connect-shaped) runs * AHEAD of the app - it owns module/HMR/asset requests and falls through to the app's * routes for everything real. Production servers simply do not pass one. */ export type ConnectMiddleware = (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, next: (error?: unknown) => void) => void; /** * Socket-level timeouts. Node ships these OFF or generous, so a slow or idle peer can pin a * connection indefinitely; production servers want them bounded and aligned with the load * balancer in front. Each is surfaced here, defaulted to a safe value, and overridable. */ export interface SocketTimeouts { /** Time to receive the COMPLETE request headers before the socket is closed (default 60s). Slowloris defense. */ headersMs?: number; /** Time to receive the complete request (headers + body) before closing (default 5min). */ requestMs?: number; /** How long an idle keep-alive socket is held open awaiting the next request (default 5s). */ keepAliveMs?: number; /** Max requests one keep-alive socket may serve before it must reconnect (default 0 = unlimited). */ maxRequestsPerSocket?: number; /** * How often the server sweeps for connections that blew past headersTimeout/requestTimeout * (default 30000 ms - Node's default). The timeouts above are enforced ON this sweep, so a * slowloris is reclaimed within headersTimeout + this interval; lower it to reclaim sooner. */ checkIntervalMs?: number; } /** * Serves an app over HTTP/1.1. `port: 0` binds an ephemeral port (the testing default). * `before` mounts a connect-style middleware ahead of the app - the dev-server seam * (`serve(app, { before: vite.middlewares })`); see {@link ConnectMiddleware}. `timeouts` * tunes the socket-level limits (all bounded by default; see {@link SocketTimeouts}). * On an interactive dev terminal the AzerothJS banner announces the bound addresses and * the measured ready time; `banner: false` silences it (it is always silent when piped * or in production). */ export declare function serve(app: WebHandler, options?: { port?: number; hostname?: string; before?: ConnectMiddleware; timeouts?: SocketTimeouts; banner?: boolean; trustProxy?: TrustProxyOptions; }): Promise>; /** * Serves an app over cleartext HTTP/2 (h2c) - the same listener, the http2 compat surface. * Browsers only speak h2 over TLS; h2c is for internal hops, proxies, and gRPC-style peers. * Sessions are bounded like http1 sockets: an inactivity timeout reclaims a dead or stalled * session (`timeouts.requestMs`, same default as {@link serve}), and a session that floods * invalid frames or rejected streams is closed. */ export declare function serveH2c(app: WebHandler, options?: { port?: number; hostname?: string; timeouts?: SocketTimeouts; maxConcurrentStreams?: number; trustProxy?: TrustProxyOptions; }): Promise>; /** Options for {@link handleShutdownSignals}. */ export interface ShutdownSignalOptions { /** Which signals trigger the drain (default SIGTERM + SIGINT - the orchestrator and Ctrl-C signals). */ signals?: NodeJS.Signals[]; /** * Forwarded to `shutdown()`: the bound on the whole drain (default 10s). In-flight * requests get up to this long to finish; upgraded sockets (WebSockets) are destroyed as * soon as that work has drained rather than waiting the period out, so a held socket * costs milliseconds on restart, not the full grace. */ gracePeriodMs?: number; /** Called if the drain itself throws; the process still exits. */ onError?: (error: unknown) => void; /** * Runs BEFORE the drain, while connections are still live: closing WebSockets with 1001, * detaching a socket server, flipping a readiness probe. Awaited. Its throw goes to * `onError` and the drain proceeds anyway - a failed pre-stop must not leave the process * holding its port. */ beforeShutdown?: () => void | Promise; /** * Runs AFTER the server has drained, BEFORE the process exits - the seam for * everything else that needs a graceful stop beside the HTTP server: a cron * scheduler's `stop({ drain: true })`, a log sink's `close()`, a DB pool's end. * Its throw goes to `onError`; the process still exits. */ beforeExit?: () => void | Promise; /** Process-exit hook, injectable for tests (default `process.exit`). */ exit?: (code: number) => void; } /** * Wires a graceful drain to process signals: on SIGTERM/SIGINT the server stops accepting, * lets in-flight requests finish (up to the grace period), destroys held sockets (upgraded * WebSockets included - a live socket exits in milliseconds, not after the grace), then exits * 0. This is the piece that makes rolling deploys and orchestrator restarts stop dropping * requests mid-flight AND stop hanging until SIGKILL - the incumbents leave it to a process * manager and hope. An app that wants clean WebSocket closes (1001) sends them from * `beforeShutdown`, which runs while the connections are still live. Returns a disposer that * removes the listeners (so tests and re-wiring do not leak process handlers). */ export declare function handleShutdownSignals(served: Served, options?: ShutdownSignalOptions): () => void; export {};