import * as _fastify_cookie from '@fastify/cookie'; import { CookieSerializeOptions, FastifyCookieOptions, Signer } from '@fastify/cookie'; export { CookieSerializeOptions, UnsignResult as CookieUnsignResult } from '@fastify/cookie'; import { FastifyRequest, FastifyReply, FastifyPluginAsync, FastifyPluginCallback, FastifyInstance, preHandlerHookHandler, FastifySchema, FastifyBaseLogger } from 'fastify'; import { APIResponseHelpers } from 'unirend/api-envelope'; import { StaticContentCache } from 'unirend/utils'; interface PageMetadata { title: string; description: string; keywords?: string; canonical?: string; og?: { title?: string; description?: string; image?: string; }; } /** * Base meta structure with required page metadata */ interface BaseMeta { page?: PageMetadata; } interface ErrorDetails { [key: string]: unknown; } /** * Error details value - supports both object and array formats * * @example Object format (structured errors) * { field: 'email', reason: 'invalid format', code: 'VALIDATION_ERROR' } * * @example Array format (multiple validation errors with type field) * [ * { field: 'email', type: 'invalid_email', message: 'Must be a valid email address' }, * { field: 'password', type: 'invalid_length', message: 'Must be at least 8 characters long' } * ] * * @example Array format (error trace) * ['Step 1 failed', 'Rollback initiated', 'Cleanup completed'] */ type ErrorDetailsValue = ErrorDetails | unknown[]; /** * Error object structure for API error responses */ interface ErrorObject { code: string; message: string; details?: ErrorDetailsValue; } interface RedirectInfo { target: string; permanent: boolean; preserve_query?: boolean; } /** * API Success Response with extensible meta * * @template T - The data type * @template M - Additional meta properties (extends BaseMeta) * * @example * // Basic usage (no extra meta) * type BasicResponse = APISuccessResponse; * * @example * // With required extra meta fields * interface CustomMeta extends BaseMeta { * pagination: { page: number; total: number }; * cache: { expires: string }; * } * type PaginatedResponse = APISuccessResponse; */ interface APISuccessResponse { status: 'success'; status_code: number; request_id: string; request_timestamp?: string; type: 'api'; data: T; meta: M; error?: null; } /** * API Error Response with extensible meta * * @template M - Additional meta properties (extends BaseMeta) */ interface APIErrorResponse { status: 'error'; status_code: number; request_id: string; request_timestamp?: string; type: 'api'; data: null; meta: M; error: ErrorObject; } /** * API response envelope as a discriminated union * * @template T - The data type for success responses * @template M - Additional meta properties (extends BaseMeta) */ type APIResponseEnvelope = APISuccessResponse | APIErrorResponse; /** * Page Success Response with extensible meta * * @template T - The data type * @template M - Additional meta properties (extends BaseMeta) */ interface PageSuccessResponse { status: 'success'; status_code: number; request_id: string; request_timestamp?: string; type: 'page'; data: T; meta: M; error?: null; ssr_request_context?: Record; } /** * Page Error Response with extensible meta * * @template M - Additional meta properties (extends BaseMeta) */ interface PageErrorResponse { status: 'error'; status_code: number; request_id: string; request_timestamp?: string; type: 'page'; data: null; meta: M; error: ErrorObject; ssr_request_context?: Record; } /** * Page Redirect Response with extensible meta * * @template M - Additional meta properties (extends BaseMeta) */ interface PageRedirectResponse { status: 'redirect'; status_code: 200; request_id: string; request_timestamp?: string; type: 'page'; data: null; meta: M; error?: null; redirect: RedirectInfo; ssr_request_context?: Record; } /** * Page response envelope as a discriminated union * * @template T - The data type for success responses * @template M - Additional meta properties (extends BaseMeta) */ type PageResponseEnvelope = PageSuccessResponse | PageErrorResponse | PageRedirectResponse; /** * Parameters passed to page data loader handlers with shortcuts to common fields * * Handlers should treat these params as the authoritative routing context * (routeParams, queryParams, requestPath, originalURL) produced by the * page data loader. Do not reconstruct routing info from the Fastify request. * * The Fastify request represents the original HTTP request and should be used * only for transport/ambient data (cookies, headers, IP, auth tokens, etc.). * During SSR, this is the same request that initiated the page render; after * hydration, client-side page data loader calls will include their own * transport context as appropriate. */ interface PageDataHandlerParams { pageType: string; version?: number; /** Indicates how the handler was invoked: via HTTP route or internal short-circuit */ invocationOrigin: 'http' | 'internal'; /** Route params (from React Router via POST body) */ routeParams: Record; /** Query params (from React Router via POST body) */ queryParams: Record; /** Request path (from React Router via POST body) */ requestPath: string; /** Original URL (from React Router via POST body) */ originalURL: string; /** The APIResponseHelpers class configured on this server (use this instead of importing directly) */ APIResponseHelpers: APIResponseHelpersClass; } /** * Handler function type for page data endpoints * * @param request - The Fastify request object (original request). Use for cookies, headers, IP, etc. * @param params - Page data context (preferred for page routing: path, query, route params) * @returns A PageResponseEnvelope (recommended), APIResponseEnvelope (will be converted), or false if response already sent * * **Recommendation**: Return PageResponseEnvelope for optimal performance and control. * APIResponseEnvelope is supported but will be converted by the pageDataLoader, which * adds overhead and may not preserve all metadata as intended. * * **Return false** when you've sent a custom response (e.g., using * APIResponseHelpers.sendErrorEnvelope() or validation helpers like ensureJSONBody). * This signals that the handler has already sent the response and the framework * should not attempt to send anything. */ type PageDataHandler = ( /** Original HTTP request (for cookies/headers/IP/auth) */ originalRequest: FastifyRequest, reply: ControlledReply, params: PageDataHandlerParams) => Promise | APIResponseEnvelope | false> | PageResponseEnvelope | APIResponseEnvelope | false; /** * Supported HTTP methods for API routes */ type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; /** * Handler function type for generic API endpoints * * Handlers should return an APIResponseEnvelope. Returning any other object * will result in a runtime error being thrown during request handling. * * **Return false** when you've sent a custom response (e.g., using * APIResponseHelpers.sendErrorEnvelope() or validation helpers like ensureJSONBody). * This signals that the handler has already sent the response and the framework * should not attempt to send anything. */ type APIRouteHandler = (request: FastifyRequest, controlledReply: ControlledReply, params: { /** HTTP method used for this route */ method: HTTPMethod; /** Endpoint path segment after version/prefix (e.g., "users/:id") */ endpoint: string; /** Registered version for this handler (always set, defaults to 1) */ version: number; /** Full path used to register the route */ fullPath: string; /** Route params extracted from Fastify (raw values) */ routeParams: Record; /** Query params extracted from Fastify (raw values) */ queryParams: Record; /** Path portion of the URL without query string */ requestPath: string; /** Original URL including query string */ originalURL: string; /** The APIResponseHelpers class configured on this server (use this instead of importing directly) */ APIResponseHelpers: APIResponseHelpersClass; }) => APIResponseEnvelope | Promise> | false | Promise; /** * Parsed domain information derived from a request hostname. * * Used both on the Fastify request object (`request.domainInfo`) and in the * Unirend React context (`useDomainInfo()`). */ interface DomainInfo { /** Bare hostname with port stripped (IPv6-safe, e.g. `'app.example.com'` or `'::1'`). */ hostname: string; /** * Apex domain without a leading dot (e.g. `'example.com'`). * Empty string for localhost and raw IP addresses where no root domain can be resolved. * * When empty, omit the `domain` attribute entirely — `domain=.localhost` is invalid * per RFC 6265 and browsers reject it. A cookie without a `domain` attribute becomes * a host-only cookie scoped to the exact hostname, which is correct for localhost and IPs. * * Prepend `.` when using as a cookie `domain` attribute to span subdomains: * ```ts * document.cookie = [ * 'theme=dark', * 'path=/', * 'max-age=31536000', * domainInfo.rootDomain ? `domain=.${domainInfo.rootDomain}` : null, * ].filter(Boolean).join('; '); * ``` */ rootDomain: string; } /** * Normalized client identity for a request, resolved by the server before plugins run. * * The canonical request accessors are `request.clientIP` (the real end user), * `request.connectionIP` (the connecting IP), `request.userAgent` (the * immediate-hop User-Agent), and `request.clientUserAgent` (the resolved real * end-user User-Agent). This frozen object mirrors the resolved end-user values * alongside correlation and forwarding metadata. `isIPFromHeader` is true when * `clientIP` came from a trusted `X-SSR-Original-IP` header rather than the * connection. */ interface ClientInfo { requestID: string; correlationID: string; /** True when request came from SSR layer with trusted forwarded headers */ isFromSSRServerAPICall: boolean; /** The connecting IP (mirrors request.connectionIP). */ connectionIP: string; /** Resolved real end-user IP (mirrors request.clientIP). */ clientIP: string; userAgent: string; /** True when clientIP was taken from a trusted X-SSR-Original-IP header */ isIPFromHeader: boolean; /** True when userAgent was taken from a trusted X-SSR-Forwarded-User-Agent header */ isUserAgentFromHeader: boolean; } declare module 'fastify' { interface FastifyRequest { clientInfo?: ClientInfo; } } type APIResponseHelpersClass = typeof APIResponseHelpers; /** * Plugin metadata returned by plugins for dependency tracking and cleanup */ interface PluginMetadata { /** Unique name for this plugin */ name: string; /** Plugin dependencies - other plugin names that must be registered first */ dependsOn?: string | string[]; } /** Server context a plugin can be registered in. */ type UnirendServerMode = 'ssr' | 'api' | 'plain'; type PluginModeWithEnvelopeHelpers = Exclude; interface PluginAPIRouteShortcuts { get(endpoint: string, handler: APIRouteHandler): void; get(endpoint: string, version: number, handler: APIRouteHandler): void; post(endpoint: string, handler: APIRouteHandler): void; post(endpoint: string, version: number, handler: APIRouteHandler): void; put(endpoint: string, handler: APIRouteHandler): void; put(endpoint: string, version: number, handler: APIRouteHandler): void; delete(endpoint: string, handler: APIRouteHandler): void; delete(endpoint: string, version: number, handler: APIRouteHandler): void; patch(endpoint: string, handler: APIRouteHandler): void; patch(endpoint: string, version: number, handler: APIRouteHandler): void; } interface PluginPageDataHandlerShortcuts { register(pageType: string, handler: PageDataHandler): void; register(pageType: string, version: number, handler: PageDataHandler): void; } /** * Plugin registration function type. * * - `ServerPlugin<'ssr'>`: SSR server plugin with envelope helpers. * - `ServerPlugin<'api'>`: API server plugin with envelope helpers. * - `ServerPlugin<'plain'>`: plain web server plugin with raw routes only. * - `ServerPlugin`: all-mode plugin that uses only the * shared host surface. It cannot call `pluginHost.api.*` or * `pluginHost.pageDataHandler.*` unless it first narrows out plain mode. * * The default `ServerPlugin` type is for envelope-capable SSR/API plugins. */ type ServerPlugin = (pluginHost: PluginHostInstance, options: PluginOptions) => Promise | PluginMetadata | void; /** * Fastify hook names that plugins can register * Includes common lifecycle hooks plus string for custom hooks */ type FastifyHookName = Parameters[0]; /** * Controlled Fastify instance interface for plugins * Exposes safe methods while preventing access to destructive operations */ interface PluginHostBase { /** Register plugins and middleware */ register: = Record>(plugin: FastifyPluginAsync | FastifyPluginCallback, opts?: Options) => Promise; /** Add custom hooks */ addHook: (hookName: FastifyHookName, handler: (request: FastifyRequest, reply: FastifyReply, ...args: unknown[]) => unknown) => void; /** Add decorators to request/reply objects */ decorate: (property: string, value: unknown) => void; decorateRequest: (property: string, value: unknown) => void; decorateReply: (property: string, value: unknown) => void; /** Read-only accessors for server-level decorations */ hasDecoration: (property: string) => boolean; getDecoration: (property: string) => T | undefined; /** Access to route registration with constraints */ route: (opts: SafeRouteOptions) => void; get: (path: string, handler: RouteHandler) => void; post: (path: string, handler: RouteHandler) => void; put: (path: string, handler: RouteHandler) => void; delete: (path: string, handler: RouteHandler) => void; patch: (path: string, handler: RouteHandler) => void; /** Server-level logger (pino). Use `(obj, msg)` argument order. Useful for logging during plugin setup, before any request exists. */ log: FastifyBaseLogger; /** The APIResponseHelpers class configured on this server — use this to build envelopes so custom subclasses are respected */ APIResponseHelpers: APIResponseHelpersClass; } interface PluginHostEnvelopeHelpers { /** API route registration shortcuts method for versioned envelope endpoints */ api: PluginAPIRouteShortcuts; /** Page data loader handler registration method for page data endpoints */ pageDataHandler: PluginPageDataHandlerShortcuts; } interface PluginHostPlainHelpers { /** Not available in plain web mode. Use raw pluginHost.get/post routes instead. */ api?: never; /** Not available in plain web mode. Use raw pluginHost.get/post routes instead. */ pageDataHandler?: never; } type PluginHostInstance = M extends 'plain' ? PluginHostBase & PluginHostPlainHelpers : PluginHostBase & PluginHostEnvelopeHelpers; /** * Controlled reply surface available to handlers. * Allows setting headers and cookies without giving full reply control. * * Used by page data loader handlers, API route handlers, and processFileUpload(). * Provides limited access to prevent handlers from prematurely sending responses * or bypassing the framework's envelope pattern. */ interface ControlledReply { /** Set a response header (content-type may be enforced by framework) */ header: (name: string, value: string) => void; /** Set a cookie if @fastify/cookie is registered */ setCookie?: (name: string, value: string, options?: CookieSerializeOptions) => void; /** Alias for setCookie if @fastify/cookie is registered */ cookie?: (name: string, value: string, options?: CookieSerializeOptions) => void; /** Clear a cookie if @fastify/cookie is registered */ clearCookie?: (name: string, options?: CookieSerializeOptions) => void; /** Verify and unsign a cookie value if @fastify/cookie is registered */ unsignCookie?: (value: string) => { valid: true; renew: boolean; value: string; } | { valid: false; renew: false; value: null; }; /** Sign a cookie value if @fastify/cookie is registered */ signCookie?: (value: string) => string; /** Read a response header value (if available) */ getHeader: (name: string) => string | number | string[] | undefined; /** Read all response headers as a plain object */ getHeaders: () => Record; /** Remove a response header by name */ removeHeader: (name: string) => void; /** Check if a response header has been set */ hasHeader: (name: string) => boolean; /** Whether the reply has already been sent */ sent: boolean; /** * Access to the underlying response stream (for connection monitoring) * * Limited scope: Only used internally by processFileUpload() for detecting * broken connections during file uploads. Most handlers won't need this */ raw: { /** Whether the underlying connection has been destroyed */ destroyed: boolean; }; /** * Internal: send an error envelope and finish the response immediately. * * This is installed by the framework's controlled-reply wrapper so helpers * can terminate with the same raw/hijacked path while still reusing shared * header logic such as CORS application. * * ControlledReply intentionally does not expose general-purpose send/write * methods to user handlers. This internal escape hatch exists only so * framework-owned helpers like APIResponseHelpers can terminate early in a * controlled way without reopening arbitrary reply access. */ _sendErrorEnvelope: (statusCode: number, errorEnvelope: APIErrorResponse | PageErrorResponse) => Promise; } /** * Safe route options that prevent catch-all conflicts */ interface SafeRouteOptions { method: string | string[]; url: string; handler: RouteHandler; preHandler?: preHandlerHookHandler | preHandlerHookHandler[]; schema?: FastifySchema; config?: unknown; constraints?: { /** Only allow specific hosts, no wildcards that could conflict with SSR */ host?: string; /** Only allow specific versions */ version?: string; }; } /** * Handler for raw plugin routes (`pluginHost.get/post/...`). * * Mirrors Fastify's own handler contract (`Return | void | Promise<...>`): you * can return the response payload synchronously, return nothing and dispatch via * `reply.send()`, or return a promise. `async` is never required for handlers * that don't await. */ type RouteHandler = (request: FastifyRequest, reply: FastifyReply) => unknown; /** * Shared configuration for versioned API endpoint groups * Used by helpers that register versioned endpoints (page data, generic API routes, etc.) */ interface APIEndpointConfig { /** * Endpoint prefix that comes before version/endpoint (default: "/api") * Set to `false` to disable API handling (server becomes a plain web server) */ apiEndpointPrefix?: string | false; /** Whether to enable versioning (default: true) */ versioned?: boolean; /** Base endpoint name for page data loader handlers (default: "page_data"). Used by SSR/APIServer's page-data registration only. */ pageDataEndpoint?: string; } /** * Plugin options passed to each plugin * * Environment information available at plugin registration time. * * Notes: * - Use these fields inside your plugin setup to decide what to REGISTER * (e.g., which routes, which hooks). This is registration-time context. * - For per-request branching inside handlers/middleware, read * `request.isDevelopment` (decorated by the servers). Both reflect the same * underlying mode; they serve different scopes. */ interface PluginOptions { /** Type of server the plugin is running on */ serverType: M; /** Server mode (development or production) */ mode: 'development' | 'production'; /** Whether running in development mode */ isDevelopment: boolean; /** API endpoints configuration from the server */ apiEndpoints?: APIEndpointConfig; } /** * Configuration for a folder in the static router folderMap */ interface FolderConfig { /** Path to the directory */ path: string; /** Whether to detect and use immutable caching for fingerprinted assets */ detectImmutableAssets?: boolean; } /** * Options for the static router middleware * Used to serve static files in production SSR mode */ interface StaticContentRouterOptions { /** Exact URL → absolute file path (optional) */ singleAssetMap?: Record; /** URL prefix → absolute directory path (as string) or folder config object */ folderMap?: Record; /** Response compression for buffered static responses; default true */ compression?: boolean | ResponseCompressionOptions; /** Maximum size (in bytes) for hashing & in-memory caching; default 5 MB */ smallFileMaxSize?: number; /** Maximum number of entries in ETag/content caches; default 100 */ cacheEntries?: number; /** Maximum total memory size (in bytes) for content cache; default 50 MB */ contentCacheMaxSize?: number; /** Maximum number of entries in the stat cache; default 250 */ statCacheEntries?: number; /** TTL in milliseconds for negative stat cache entries; default 30 seconds */ negativeCacheTtl?: number; /** TTL in milliseconds for positive stat cache entries; default 1 hour */ positiveCacheTtl?: number; /** Custom Cache-Control header; default 'public, max-age=0, must-revalidate' */ cacheControl?: string; /** Cache-Control header for immutable fingerprinted assets; default 'public, max-age=31536000, immutable' */ immutableCacheControl?: string; } interface ResponseCompressionOptions { /** Whether compression is enabled when using the object form */ enabled?: boolean; /** Minimum response size in bytes before compression is attempted */ threshold?: number; /** Prefer Brotli over gzip when the client gives both equal q-values */ preferBrotli?: boolean; /** Brotli quality level passed to Node.js zlib */ brotliQuality?: number; /** gzip compression level passed to Node.js zlib */ gzipLevel?: number; } declare module 'fastify' { interface FastifyRequest { /** * Active SSR app key for multi-app routing. * * Read-only request value. Defaults to `'__default__'`. * Use `request.setActiveSSRApp(appKey)` in SSR middleware to select a * registered app and refresh app-derived request values. */ readonly activeSSRApp: string; /** * Select the active SSR app for this request. * * Validates that the app exists, updates `request.activeSSRApp`, refreshes * `request.publicAppConfig`, and updates the app-level CDN default unless * middleware already overrode `request.CDNBaseURL`. */ setActiveSSRApp: (appKey: string) => void; /** * Resolved connection IP — the direct connection. * * Set once per request by the framework using `getConnectionIP` (if * provided) or falling back to `request.ip` (which reflects Fastify proxy * handling when `fastifyOptions.trustProxy` is configured). For debugging * and "who connected to me" decisions. (For per-user rate limiting prefer * `clientIP` — `connectionIP` can be a shared CDN/proxy address.) * * Available throughout the request lifecycle and as the access-log * `{{connectionIP}}` template variable. */ connectionIP: string; /** * Resolved real end-user IP. * * Starts as `connectionIP`, then — when the `clientInfo` resolution is * enabled (default) and the connection is trusted — is replaced with the * original browser IP forwarded by an SSR server (`X-SSR-Original-IP`). Use * this for end-user attribution and per-user logic like rate limiting; it * sees through CDNs/load balancers and the SSR → API hop. Equals * `connectionIP` for direct requests or when `clientInfo` is disabled. * * Available throughout the request lifecycle and as the access-log `{{ip}}` * template variable. */ clientIP: string; /** * Immediate-hop User-Agent header. * * This is the raw `User-Agent` request header normalized to a string, or * `''` when absent. It is not changed by trusted SSR forwarding. */ userAgent: string; /** * Resolved real end-user User-Agent. * * Starts as `userAgent`, then — when the `clientInfo` resolution is enabled * (default) and the connection is trusted — is replaced with the original * browser User-Agent forwarded by an SSR server * (`X-SSR-Forwarded-User-Agent`). Equals `userAgent` for direct requests or * when `clientInfo` is disabled. * * Available throughout the request lifecycle and as the access-log * `{{userAgent}}` template variable. */ clientUserAgent: string; /** * Normalized client identity (correlation ID, forwarded-source flags, * resolved User-Agent). Populated by client-info resolution when * enabled (default); `undefined` when the `clientInfo` option is `false`. */ clientInfo?: ClientInfo; /** * Unique request identifier, set once per request by the framework before * access logging and plugins run. * * Defaults to a ULID (globally unique, safe across instances/restarts). * Customizable via the `getRequestID` server option. The API/Page envelope * helpers read this for the envelope `request_id` field, and it is available * throughout the request lifecycle (access-log templates `{{requestID}}` and * hooks, plugins, page data + API route handlers). * * Distinct from Fastify's `request.id` (an incremental per-process counter * surfaced as the access log `reqID`). `undefined` only if a custom * `getRequestID` opted out by returning `undefined` or an empty string. */ requestID?: string; /** * Server label for this instance (e.g. `'SSR'`, `'API'`). * * Set once per request by the framework from the `serverLabel` server option. * Available in access log templates as `{{serverLabel}}` and throughout * the request lifecycle via `request.serverLabel`. */ serverLabel: string; /** * Safe-to-share app configuration cloned and frozen for this request. * * Available to plugins, handlers, and helpers on SSR and API servers. * SSR also exposes it to React and injects it into HTML. API servers only * send it to clients if your response includes selected values. */ publicAppConfig?: Record; /** * Effective CDN base URL for this SSR request. * * SSR middleware can set this as a per-request override. If it remains * unset, the SSR server populates it from the active app's `CDNBaseURL` * before preHandler hooks, route handlers, SSR render, and custom 500 * pages run. * * API servers do not set this field. */ CDNBaseURL?: string; /** * Optional request-scoped helper installed by the built-in CORS plugin. * * Raw/hijacked response paths can call this before `writeHead(...)` to * apply the same actual-response CORS/security headers that normal * Fastify-managed responses receive. */ applyCORSHeaders?: (reply: FastifyReply) => void | Promise; /** * Internal request-start timestamp captured by the framework. * * Used by framework features that need a stable "request received" time, * including API/page envelope timestamps and fallback response-time * calculation when Fastify's built-in elapsedTime is unavailable. */ receivedAt?: number; /** * Per-request mutable key-value store initialized by both SSRServer and APIServer * before any plugins or hooks run. * * Use this to pass data between middleware, hooks, and handlers — for example, * seeding user session info, theme preferences, or CSRF tokens from an onRequest * hook so that page data loader handlers and API handlers can read them. * * During SSR, the server injects the final context into the rendered HTML so * client-side React can hydrate with the same values via `useRequestContext()`. * * In separated SSR/API deployments the SSR layer forwards this context to trusted * API page data requests and merges returned context back automatically. */ requestContext: Record; /** * Parsed domain information for this request, computed once per request from * `request.hostname` by both SSRServer and APIServer. * * - `hostname`: bare hostname with port stripped (IPv6-safe) * - `rootDomain`: apex domain without a leading dot (e.g. `'example.com'`); * empty string for localhost and raw IP addresses * * Use `rootDomain` to set subdomain-spanning cookies by prepending a dot: * `domain=.${request.domainInfo.rootDomain}` — the same value that the * client-side `cycleTheme()` helper uses. */ domainInfo: DomainInfo; /** * Set to `true` when the request is handled by the built-in static content * handler (fingerprinted assets, public files, etc.), regardless of which * server type is serving the file. * * Initialized to `false` for every request. The static content handler sets * it to `true` before calling `reply.hijack()`, so it is observable in * `onResponse` hooks and access log templates even though `onSend` is bypassed. * * Use this to skip work that is inappropriate for static asset responses — * for example, cookie renewal should only happen on document/API responses, * not on every `.js` or `.css` file request. */ isStaticAsset: boolean; } } /** * CORS origin configuration - can be a string, array, or function */ type CORSOrigin = string | string[] | ((origin: string | undefined, request: FastifyRequest) => boolean | Promise); /** * Configuration for dynamic CORS handling */ interface CORSConfig { /** * Allowed origins for CORS requests * - string: Single origin (e.g., "https://example.com") * - string[]: Multiple origins with wildcard support * - function: Dynamic origin validation * - "*": Allow all origins (not recommended with credentials) * * Wildcard patterns supported: * - "*.example.com": Direct subdomains only (api.example.com ✅, app.api.example.com ❌) * - "**.example.com": All subdomains including nested (api.example.com ✅, app.api.example.com ✅) * - "https://*": Any domain with HTTPS protocol * - "http://*": Any domain with HTTP protocol * - "https://*.example.com": HTTPS subdomains only * - "http://**.example.com": HTTP subdomains including nested * * Note: "null" origins (from sandboxed documents, file:// URLs) are treated as regular string values. * Include "null" in your origin array or handle it in your validation function if needed. * * @default "*" */ origin?: CORSOrigin; /** * Origins that are allowed to send credentials (cookies, auth headers) * This enables more granular control than standard CORS libraries * * - string[]: List of trusted origins that can send credentials * - function: Dynamic credential validation based on origin * - true: Allow credentials for all allowed origins (same as @fastify/cors) * - false: Never allow credentials * * @default false */ credentials?: boolean | string[] | ((origin: string | undefined, request: FastifyRequest) => boolean | Promise); /** * Allowed HTTP methods * @default ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"] */ methods?: string[]; /** * Allowed request headers * - string[]: List of specific headers (e.g., ["Content-Type", "Authorization"]) * - ["*"]: Reflect exactly what the browser requests (useful for public APIs) * @default ["Content-Type", "Authorization", "X-Requested-With"] */ allowedHeaders?: string[]; /** * Headers exposed to the client * @default [] */ exposedHeaders?: string[]; /** * Max age for preflight cache (in seconds) * @default 86400 (24 hours) */ maxAge?: number; /** * Whether to pass control to next handler on preflight OPTIONS requests * @default false */ preflightContinue?: boolean; /** * Status code for successful preflight responses * @default 204 */ optionsSuccessStatus?: number; /** * Whether to allow private network requests (Chrome feature) * When true, responds to Access-Control-Request-Private-Network with Access-Control-Allow-Private-Network * @default false */ allowPrivateNetwork?: boolean; /** * Opt-in: allow wildcard subdomain patterns (e.g., "*.example.com") in `credentials` array * When true, patterns like "*.example.com", "**.example.com", "*.*.example.com" are permitted. * Apex domains are NOT matched by wildcard patterns; include the apex explicitly if needed. * Invalid patterns (bare "*", protocol wildcards like "https://*") are rejected. * * @default false */ credentialsAllowWildcardSubdomains?: boolean; /** * Opt-in: allow credentials: true when origin includes a protocol wildcard (e.g., "https://*") * By default this is disallowed for safety because it enables credentials for any origin * on that protocol. * * @default false */ allowCredentialsWithProtocolWildcard?: boolean; /** * Controls the X-Frame-Options response header. * - false: do not send the header (default) * - "DENY" | "SAMEORIGIN": header value to send * * @default false */ xFrameOptions?: false | 'DENY' | 'SAMEORIGIN'; /** * Controls the Strict-Transport-Security (HSTS) response header. * - false: do not send the header (default) * - { maxAge, includeSubDomains?, preload? }: header parameters * * Note: HSTS is typically only appropriate over HTTPS in production. * This plugin does not inspect the connection security; enable with care. * * @default false */ hsts?: false | { maxAge: number; includeSubDomains?: boolean; preload?: boolean; }; } /** * Dynamic CORS plugin for Unirend * * Provides more flexible CORS handling than @fastify/cors, specifically: * - Dynamic credentials based on origin * - Function-based origin validation * - Separate credential and origin policies * * @example * ```typescript * // Allow public API access but only credentials for trusted origins * cors({ * origin: "*", // Allow any origin for public API * credentials: ["https://myapp.com", "https://admin.myapp.com"], // Only these can send cookies * methods: ["GET", "POST"], * }) * * // Handle "null" origins from sandboxed documents or file:// URLs * cors({ * origin: ["https://app.com", "null"], // Explicitly allow null origins * credentials: ["https://app.com"], // Credentials not allowed for null origins * }) * * // Dynamic validation based on request * cors({ * origin: (origin, request) => { * // Allow any origin for public endpoints * if (request.url?.startsWith('/api/public/')) return true; * // Restrict private endpoints * return origin === 'https://myapp.com'; * }, * credentials: (origin, request) => { * // Only allow credentials for authenticated endpoints from trusted origins * return request.url?.startsWith('/api/auth/') && origin === 'https://myapp.com'; * } * }) * ``` */ declare function cors(config?: CORSConfig): ServerPlugin; /** * Response configuration for invalid domain handler */ interface InvalidDomainResponse { contentType: 'json' | 'text' | 'html'; content: string | object; } /** * Domain validation configuration - can be a string, array, or function */ type ValidProductionDomains = string | string[] | ((domain: string, request: FastifyRequest) => boolean | Promise); /** * Configuration options for the domainValidation plugin */ interface DomainValidationConfig { /** * Valid production domains that are allowed to access this server * * Can be a single domain string, array of domain strings (without protocol), * or a function for request-aware domain validation. * Wildcard patterns supported: * - "example.com" - allows exact match only * - "*.example.com" - allows direct subdomains only (api.example.com ✅, app.api.example.com ❌) * - "**.example.com" - allows all subdomains including nested (api.example.com ✅, app.api.example.com ✅) * * Examples: * - ["example.com", "www.example.com", "api.example.com"] - specific domains * - ["**.example.com", "example.com"] - apex + all subdomains (including nested) * - ["*.example.com", "example.com"] - apex + direct subdomains only * * Note: Domain validation is protocol-agnostic (ignores http/https) * If not specified, domain validation is skipped */ validProductionDomains?: ValidProductionDomains; /** * Optional canonical domain to redirect to if the request domain doesn't match * Should be defined without www prefix or protocol (use wwwHandling to control www) * If specified, requests to valid domains will be redirected to this canonical domain * If not specified, valid domains are allowed without redirection * Example: "example.com" */ canonicalDomain?: string; /** * Whether to enforce HTTPS by redirecting HTTP requests * @default true */ enforceHTTPS?: boolean; /** * How to handle www prefix normalization for apex domains only * - "remove": Strip www prefix (www.example.com → example.com) * - "add": Add www prefix (example.com → www.example.com) * - "preserve": Don't modify www, only validate canonical domain matches * Note: Only applies to apex domains, not subdomains (api.example.com stays unchanged) * @default "preserve" */ wwwHandling?: 'remove' | 'add' | 'preserve'; /** * HTTP status code to use for redirects * @default 301 (permanent redirect) */ redirectStatusCode?: 301 | 302 | 307 | 308; /** * Whether to preserve port numbers in canonical domain redirects * - true: example.com:3000 → canonical.com:3000 * - false: example.com:3000 → canonical.com (strip port) * @default false */ preservePort?: boolean; /** * Whether to skip all checks in development mode * @default true */ skipInDevelopment?: boolean; /** * Whether to trust proxy headers (x-forwarded-host/proto) when determining * the original host and protocol. Only enable this when running behind a * trusted proxy/load balancer that sets these headers. * @default false */ trustProxyHeaders?: boolean; /** * Optional custom handler for invalid domain responses * If not provided, returns a default 403 plain text or JSON error response * based on if detected as an API endpoint */ invalidDomainHandler?: (request: FastifyRequest, domain: string, isDevelopment: boolean, isAPI: boolean) => InvalidDomainResponse; } /** * Domain security plugin that handles: * - Domain validation and canonical domain redirects * - HTTPS enforcement (HTTP to HTTPS redirects) * - WWW prefix normalization (add or remove www) * * This plugin is a no-op in development mode by default. */ declare function domainValidation(config: DomainValidationConfig): ServerPlugin; type CookiesConfig = FastifyCookieOptions; /** * Built-in cookies plugin that registers @fastify/cookie and exposes dependency metadata. * * Usage: * plugins: [cookies({ secret: "your-secret" })] * * Other plugins can declare a dependency on "cookies" in their PluginMetadata.dependsOn * to ensure this plugin is registered first. */ declare function cookies(config?: CookiesConfig): ServerPlugin; /** * Creates a static content serving plugin that can be used with any Unirend server. * * This plugin serves static files from configured paths with: * - Efficient file caching and ETag support for conditional requests * - Content-based strong ETags for small files (SHA-256) * - Weak ETags for large files (size + mtime based) * - LRU caching for stats, content, and ETags * - Range request support for large files * - Immutable asset detection for fingerprinted files (optional) * * Multiple instances can be registered with different configurations, * allowing you to serve files from different directories with different settings. * * @example Basic usage - serve uploads folder * ```typescript * import { staticContent } from 'unirend/plugins'; * * const server = serveSSRWithHMR(paths, { * plugins: [ * staticContent({ * folderMap: { * '/uploads': './uploads', * '/static': './public/static', * }, * }), * ], * }); * ``` * * @example Multiple folders with different settings * ```typescript * import { staticContent } from 'unirend/plugins'; * * const server = serveSSRBuilt(buildDir, { * plugins: [ * // User uploads - no immutable caching * staticContent({ * folderMap: { * '/uploads': { path: './uploads', detectImmutableAssets: false }, * }, * }), * // Static assets with fingerprinted filenames - immutable caching * staticContent({ * folderMap: { * '/static': { path: './public/static', detectImmutableAssets: true }, * }, * }), * ], * }); * ``` * * @example Custom plugin name for debugging and dependencies * ```typescript * const server = serveSSRBuilt(buildDir, { * plugins: [ * staticContent({ * folderMap: { '/uploads': './uploads' }, * }, 'uploads-handler'), * ], * }); * ``` * * @example Use on standalone API server * ```typescript * import { serveAPI } from 'unirend/server'; * import { staticContent } from 'unirend/plugins'; * * const server = serveAPI({ * plugins: [ * staticContent({ * folderMap: { * '/files': './data/files', * }, * singleAssetMap: { * '/favicon.ico': './public/favicon.ico', * }, * }), * ], * }); * ``` * * @example Fine-tuned caching settings * ```typescript * staticContent({ * folderMap: { '/assets': './dist/assets' }, * smallFileMaxSize: 1024 * 1024, // 1MB - files below this get content-based ETags * cacheEntries: 200, // Max LRU cache entries * contentCacheMaxSize: 100 * 1024 * 1024, // 100MB total content cache * positiveCacheTtl: 3600 * 1000, // 1 hour for found files * negativeCacheTtl: 60 * 1000, // 1 minute for 404s * cacheControl: 'public, max-age=3600', // Custom Cache-Control * }) * ``` * * @example Provide external cache for runtime updates * ```typescript * import { staticContent } from 'unirend/plugins'; * import { StaticContentCache } from 'unirend/utils'; * * // Create cache externally for runtime control * const cache = new StaticContentCache({ * folderMap: { '/pages': './dist/pages' } * }); * * const server = serveSSRWithHMR(paths, { * plugins: [ * staticContent(cache, 'pages-handler'), * ], * }); * * await server.listen(3000); * * // Later: update mappings dynamically * cache.updateConfig({ * singleAssetMap: { * '/blog/new-post': './dist/blog/new-post.html' * } * }); * ``` * * @param configOrCache Static content router configuration OR an existing StaticContentCache instance * @param name Optional custom name for this plugin instance (useful for debugging and plugin dependencies) * @returns A ServerPlugin that can be added to the plugins array */ declare function staticContent(configOrCache: StaticContentRouterOptions | StaticContentCache, name?: string): ServerPlugin; declare const cookieUtils: { readonly parse: (cookieHeader: string, opts?: _fastify_cookie.ParseOptions) => { [key: string]: string; }; readonly serialize: (name: string, value: string, opts?: _fastify_cookie.SerializeOptions) => string; readonly signerFactory: _fastify_cookie.SignerFactory; readonly Signer: typeof Signer; readonly sign: _fastify_cookie.Sign; readonly unsign: _fastify_cookie.Unsign; }; export { type CORSConfig, type CORSOrigin, type CookiesConfig, type DomainValidationConfig, type FolderConfig, type InvalidDomainResponse, type StaticContentRouterOptions, type ValidProductionDomains, cookieUtils, cookies, cors, domainValidation, staticContent };