UNPKG

@thi.ng/server

Version:

Minimal HTTP server with declarative routing, static file serving and freely extensible via pre/post interceptors

241 lines 8.08 kB
import type { Fn, Fn2, Maybe, MaybePromise } from "@thi.ng/api"; import type { ILogger } from "@thi.ng/logger"; import type { Route, RouteMatch } from "@thi.ng/router"; import type { IncomingMessage } from "node:http"; import type { BlockList } from "node:net"; import type { Server, ServerResponse } from "./server.js"; export type Method = "get" | "put" | "post" | "delete" | "head" | "options" | "patch"; export interface ServerOpts<CTX extends RequestCtx = RequestCtx> { logger: ILogger; /** * SSL configuration */ ssl: { key: string; cert: string; }; /** * Host name this server should respond to. * * @defaultValue `localhost` */ host: string; /** * Port number. * * @defaultValue 443 / 8080 */ port: number; /** * Initial list of routes (more can be added dynamically via * {@link Server.addRoutes}). */ routes: ServerRoute<CTX>[]; /** * Route prefix. Default: `/`. All routes are assumed to have this prefix * prepended. If given, the prefix MUST end with `/`. */ prefix: string; /** * If true (default), the trailing slash of a given route input string will * be removed before matching. */ trim: boolean; /** * Interceptors (aka pre/post middlewares) which are to be applied to all * route handlers (in the given order). */ intercept: Interceptor<CTX>[]; /** * User defined function to augment the {@link RequestCtx} object for each * request before processing its handler & interceptors. */ context: Fn<RequestCtx, CTX>; /** * Timeout in milliseconds for receiving the entire request from the client. */ requestTimeout: number; /** * List of response headers that should be sent only once. */ uniqueHeaders: string[]; /** * Used for disabling inbound access to specific IP addresses, IP ranges, or * IP subnets. This does not work if the server is behind a reverse proxy. * * @remarks * Reference: https://nodejs.org/api/net.html#class-netblocklist */ blockList: BlockList; /** * HTTP method adapter/converter. The default implementation only converts * {@link Method} `head` to `get` if the route does not provide a `head` * handler. */ method: Fn2<Method, Pick<RequestCtx, "req" | "route" | "match" | "cookies" | "query">, Method>; } export interface ServerRoute<CTX extends RequestCtx = RequestCtx> extends Route { handlers: Partial<Record<Method, RequestHandler<CTX>>>; } /** * Version of {@link ServerRoute} whose handlers/interceptors already have been * pre-processed. */ export interface CompiledServerRoute<CTX extends RequestCtx = RequestCtx> extends Route { handlers: Partial<Record<Method, CompiledHandler<CTX>>>; } export interface RequestCtx { /** * Server instance. */ server: Server; /** * Logger instance. */ logger: ILogger; /** * Parsed request URL. * * @remarks * Route handlers SHOULD only use this url instead of * {@link RequestCtx.req}'s `.url` property. This ensures, that the * hash-fragment (`#...`) part is properly dealt with, avoiding security * issues related to HTTP `request-target` * (https://datatracker.ietf.org/doc/html/rfc9112#section-3.2). * * Note: The server already responds to any request containing a `#` in its * URL with a HTTP 400. * * Also see {@link RequestCtx.path} and {@link RequestCtx.query}. */ url: URL; /** * HTTP request instance. */ req: IncomingMessage; /** * HTTP server response instance. */ res: ServerResponse; /** * Pre-compiled route handler and its interceptors */ route: CompiledServerRoute; /** * Parsed route details. */ match: RouteMatch; /** * Request method (possibly adapted). Also see {@link RequestCtx.origMethod}. */ method: Method; /** * Original request method. Also see {@link RequestCtx.method}. */ origMethod: Method; /** * URI-decoded path part of request URL. */ path: string; /** * Parsed query string params (aka URL search params). */ query: Record<string, any>; /** * Parsed cookies, if any. */ cookies?: Record<string, string>; /** * Server session (only if {@link SessionInterceptor} is used). */ session?: ServerSession; } export type HandlerResult = MaybePromise<void>; export type PreInterceptorResult = MaybePromise<boolean>; export type PostInterceptorResult = MaybePromise<void>; export type RequestHandler<CTX extends RequestCtx = RequestCtx> = Fn<CTX, HandlerResult> | InterceptedRequestHandler<CTX>; export interface InterceptedRequestHandler<CTX extends RequestCtx = RequestCtx> { fn: Fn<CTX, HandlerResult>; /** * List of interceptors which will be executed when processing the main * handler {@link InterceptedRequestHandler.fn}. * * @remarks * Handler-specific interceptors will be appended to the list global * interceptors (aka {@link ServerOpts.intercept}). Processing then happens * in this order: * * - {@link Interceptor.pre} interceptors in the order given * - {@link InterceptedRequestHandler.fn} main handler * - {@link Interceptor.post} interceptors in the REVERSE order given * * If an interceptor function returns false, further processing stops and * response will be closed. */ intercept: Interceptor<CTX>[]; } export interface CompiledHandler<CTX extends RequestCtx = RequestCtx> { fn: Fn<CTX, HandlerResult>; pre?: Maybe<Fn<CTX, PreInterceptorResult>>[]; post?: Maybe<Fn<CTX, PostInterceptorResult>>[]; } export interface Interceptor<CTX extends RequestCtx = RequestCtx> { /** * Interceptor function which will be run BEFORE the main route handler (aka * {@link InterceptedRequestHandler.fn}). If an interceptor needs to cancel * the request processing it must return `false`. In this case any further * pre-interceptors and the main handler will be skipped. In the post-phase, * only the interceptors preceding the failed one will be run (though in * reverse order). E.g. If the 3rd pre-interceptor failed, only the post * phases of the first two will still be run (if available)... */ pre?: Fn<CTX, PreInterceptorResult>; /** * Interceptor function which will be run AFTER the main route handler (aka * {@link InterceptedRequestHandler.fn}). Post-interceptors cannot cancel * request processing and are mainly intended for logging or clean-up * purposes. Post interceptors */ post?: Fn<CTX, PostInterceptorResult>; } export interface ServerSession { /** * Unique session ID */ id: string; /** * Client's remote IP address when session was originally created. To * counteract session fixation, each request's remote address is being * checked (by {@link SessionInterceptor.pre}) against this stored address. * If there's a mismatch between the two, then a new session will be * generated automatically. */ ip: string; } export interface ISessionStore<T extends ServerSession = ServerSession> { /** * Attempts to retrieve the session for given `id`. * * @param id */ get(id: string): MaybePromise<Maybe<T>>; /** * Adds to or updates given `session` in underlying storage. * * @param session */ set(session: T): MaybePromise<boolean>; /** * Attempts to delete the session for given `id` from storage. Returns true * if successful. * * @param id */ delete(id: string): MaybePromise<boolean>; /** * Configured Time-To-Live for stored sessions. Will also be used to * configure the `max-age` attribute of the session ID cookie. */ readonly ttl: number; } //# sourceMappingURL=api.d.ts.map