import * as _fastify_cookie from '@fastify/cookie'; import { FastifyCookieOptions, Signer } from '@fastify/cookie'; export { CookieSerializeOptions, UnsignResult as CookieUnsignResult } from '@fastify/cookie'; import { FastifyReply, FastifyPluginAsync, FastifyPluginCallback, FastifyInstance, FastifyRequest, preHandlerHookHandler, FastifySchema, FastifyBaseLogger } from 'fastify'; import { APIResponseHelpers } from 'unirend/api-envelope'; import fs from 'fs'; /** * 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; } 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[]; } /** * Plugin registration function type * Plugins get access to a controlled subset of Fastify functionality * Can optionally return metadata for dependency tracking */ 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 PluginHostInstance { /** 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; /** API route registration shortcuts method for versioned endpoints */ api?: unknown; /** Page data loader handler registration method for page data endpoints */ pageDataHandler?: unknown; /** The APIResponseHelpers class configured on this server — use this to build envelopes so custom subclasses are respected */ APIResponseHelpers: APIResponseHelpersClass; } /** * 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; }; } type RouteHandler = (request: FastifyRequest, reply: FastifyReply) => void | Promise; /** * 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: 'ssr' | 'api'; /** 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 client IP address. * * Set once per request by the framework using `getClientIP` (if provided) * or falling back to `request.ip` (which reflects Fastify proxy handling * when `fastifyOptions.trustProxy` is configured). * * Available throughout the entire request lifecycle, including plugins, * hooks, page data loader handlers, API route handlers, and access log * templates/hooks. */ clientIP: 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; interface ClientInfo { requestID: string; correlationID: string | null; /** True when request came from SSR layer with trusted forwarded headers */ isFromSSRServerAPICall: boolean; IPAddress: string; userAgent: string; isIPFromHeader: boolean; isUserAgentFromHeader: boolean; } declare module 'fastify' { interface FastifyRequest { /** Optional unique request ID used by response helpers */ requestID?: string; clientInfo?: ClientInfo; /** Unix timestamp (ms) when the request was received — set by onRequest hook */ receivedAt?: number; } } /** * Configuration options for the clientInfo plugin */ interface ClientInfoLoggingOptions { /** Log each request with its generated request ID. Default: false */ requestReceived?: boolean; /** Log decision/details when trusting forwarded client info. Default: false */ forwardedClientInfo?: boolean; /** Warn when SSR/forwarded headers are present from untrusted source. Default: false */ rejectedForwardedHeaders?: boolean; } interface ClientInfoConfig { /** Custom function to generate request IDs. Defaults to ulid() */ requestIDGenerator?: () => string; /** Custom validator for request/correlation IDs. Defaults to ULID validation */ requestIDValidator?: (id: string) => boolean; /** If true, set X-Request-ID and X-Correlation-ID response headers. Default: true */ setResponseHeaders?: boolean; /** * Callback that determines whether to accept forwarded client-info headers. * Default: returns true when request.clientIP is private. Otherwise forwarded * headers are ignored and direct request values are used. */ trustForwardedHeaders?: (request: FastifyRequest) => boolean; /** Optional logging configuration */ logging?: boolean | ClientInfoLoggingOptions; } /** * Client Info plugin to extract and normalize client information and handle request IDs * * This middleware: * 1. Generates or forwards request IDs * 2. Handles client info from both direct requests and SSR-forwarded requests * 3. Validates SSR requests come from private IP ranges */ declare function clientInfo(config?: ClientInfoConfig): 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; /** * Minimal stat info interface with only the properties we actually use */ interface MinimalStatInfo { isFile: boolean; size: number; mtime: Date; mtimeMs: number; } /** * Options for getFile() method */ interface GetFileOptions { /** Whether to detect immutable assets for cache control decisions */ shouldDetectImmutable?: boolean; /** Optional ETag from client's If-None-Match header (for 304 optimization) */ clientETag?: string; /** Accepted content encodings from the request for representation selection */ acceptEncoding?: string | string[]; } /** * Options for creating a read stream (for range requests) */ interface CreateStreamOptions { /** Start byte position (inclusive) */ start?: number; /** End byte position (inclusive) */ end?: number; } /** * Result from serveFile() indicating what action was taken */ type ServeFileResult = { served: false; reason: 'not-found'; } | { served: false; reason: 'error'; error: Error; } | { served: true; statusCode: 200 | 206 | 304 | 400 | 416; }; /** * File content discriminated union - either buffered in memory or needs streaming */ type FileContent = { /** Content is buffered in memory (small files) */ shouldStream: false; /** The file content buffer */ data: Buffer; } | { /** Content needs to be streamed from disk (large files) */ shouldStream: true; /** Factory function to create a read stream with optional range support */ createStream: (options?: CreateStreamOptions) => fs.ReadStream; }; /** * Internal logger object used by static content helpers. */ type StaticContentWarnLoggerObject = { warn: (obj: object, msg: string) => void; }; /** * Result when file is not found (404) */ interface FileNotFoundResult { status: 'not-found'; } /** * Result when an unexpected error occurs (500) */ interface FileErrorResult { status: 'error'; error: Error; } /** * Result when client's ETag matches (304 Not Modified) */ interface FileNotModifiedResult { status: 'not-modified'; /** Generated ETag for the selected response representation */ etag: string; /** Last-Modified date as HTTP header string */ lastModified: string; /** Selected content encoding, if a compressed representation was chosen */ contentEncoding?: 'br' | 'gzip'; /** Whether the response should include `Vary: Accept-Encoding` */ varyByAcceptEncoding: boolean; } /** * Result when file is found and should be served (200) */ interface FileFoundResult { status: 'ok'; /** File stats (size, modification time, etc.) */ stat: MinimalStatInfo; /** Generated ETag for the selected response representation */ etag: string; /** Base file ETag before representation-specific encoding suffixes */ baseETag: string; /** Last-Modified date as HTTP header string */ lastModified: string; /** MIME type based on file extension */ mimeType: string; /** File content - either buffered or needs streaming */ content: FileContent; /** Selected content encoding, if a compressed representation was chosen */ contentEncoding?: 'br' | 'gzip'; /** Whether the response should include `Vary: Accept-Encoding` */ varyByAcceptEncoding: boolean; /** Whether this file appears to be fingerprinted/immutable (for aggressive caching) */ isImmutableAsset: boolean; } /** * Union type for all possible getFile() results */ type FileResult = FileNotFoundResult | FileErrorResult | FileNotModifiedResult | FileFoundResult; /** * Encapsulates caching and serving of static content files. * * This class manages: * - Multiple LRU caches (ETag, file content, and file stats) * - Configuration for single asset and folder mappings * - Optimized file serving with HTTP caching headers * - Content-based ETags for small files, weak ETags for large files * - Automatic detection of immutable assets (fingerprinted files) * * Each instance maintains its own independent caches, allowing * multiple instances with different configurations. */ declare class StaticContentCache { private singleAssetMap; private folderMap; private readonly smallFileMaxSize; private readonly cacheControl; private readonly immutableCacheControl; private readonly negativeCacheTtl; private readonly positiveCacheTtl; private readonly compression; private readonly etagCache; private readonly contentCache; private readonly compressedVariantCache; private readonly statCache; private readonly compressedContentIndex; private readonly logger?; /** * Creates a new StaticContentCache instance * * @param options Static content configuration (file mappings, cache settings, etc.) * @param logger Optional logger (e.g., fastify.log) for error logging */ constructor(options: StaticContentRouterOptions, logger?: StaticContentWarnLoggerObject); /** * Gets file metadata and content with optimized caching * * This method handles all the core file operations and caching: * - File stats caching to avoid repeated filesystem operations * - ETag generation and caching (content-based for small files, weak for large files) * - Small file content caching in memory for performance * - Proper MIME type detection * - Immutable asset detection for cache control decisions * - Optional short-circuit if client ETag matches (for 304 responses) * * Useful for both HTTP serving (via serveFile) and programmatic access * * @param resolvedPath The absolute path to the file * @param options Optional configuration for file retrieval * @returns Result with status: 'not-found', 'error', 'not-modified', or 'ok' */ getFile(resolvedPath: string, options?: GetFileOptions): Promise; /** * Serves a static file via HTTP with conditional responses * * This is a thin HTTP wrapper around getFile() that handles: * - HTTP 304 Not Modified responses when client cache is valid (If-None-Match) * - HTTP 206 Partial Content responses for range requests * - Proper HTTP headers (Cache-Control, ETag, Content-Type, Last-Modified, etc.) * - Streaming large files vs sending cached buffers for small files * * The heavy lifting (file I/O, caching, ETag generation) is done by getFile() * * @param req The Fastify request object * @param reply The Fastify reply object * @param resolvedPath The absolute path to the file to be served * @param options Optional configuration for file serving * @returns Information about whether the file was served and what status code */ serveFile(req: FastifyRequest, reply: FastifyReply, resolvedPath: string, options?: GetFileOptions): Promise; /** * Replaces routing maps and clears all file caches in one shot. * * Use this after a full build has completed. Unlike `updateConfig`, this method * makes no attempt at smart per-path invalidation — it simply replaces * whichever maps you provide and wipes the content, stat, and ETag caches * unconditionally, guaranteeing fresh reads for the next request. * * You may pass `singleAssetMap`, `folderMap`, or both. Omitted sections retain * their current routing configuration. Pass an empty object (`{}`) for a * section to clear all mappings in that section. All file caches are always * cleared, regardless of which sections are provided — even when only * `singleAssetMap` is passed, the rebuilt HTML pages likely reference JS/CSS * bundles served from `folderMap` directories that were also regenerated in * the same build step, so preserving folder caches would risk serving stale * assets alongside fresh pages. * * For targeted cache invalidation (when URL-to-path mappings changed but * file contents at those paths are unchanged), use `updateConfig` instead. * Note: `updateConfig` does not detect in-place file content changes — it * only tracks which filesystem paths entered or left the map. * * @param newConfig Sections to replace (at least one should be provided) * * @example * ```typescript * // After an SSG build completes (page map only): * cache.replaceConfig({ singleAssetMap: await loadPageMap() }); * * // After a build that changes both pages and asset folders: * cache.replaceConfig({ * singleAssetMap: await loadPageMap(), * folderMap: { '/assets/': { path: './dist/assets', detectImmutableAssets: true } }, * }); * ``` */ replaceConfig(newConfig: { singleAssetMap?: Record; folderMap?: Record; }): void; /** * Evicts a single file's cached content, stat, and ETag without touching * any URL-to-path mappings. * * Use this when you know a specific file changed on disk and want to force * a fresh read on the next request — without flushing the entire cache. * Works for files served via `singleAssetMap` or `folderMap`. * * The parameter is the **filesystem path** (as it appears in the cache key), * not a URL. * * For `singleAssetMap` entries these are the absolute paths you * provided. * * For folder-served files the cache key is the absolute path * resolved at request time. * * @param fsPath Absolute filesystem path of the file to evict * * @example * ```typescript * // A file watcher detected /dist/about.html was rewritten: * cache.invalidateFile('/dist/about.html'); * ``` */ invalidateFile(fsPath: string): void; /** * Clears all caches (useful for testing or cache invalidation) */ clearCaches(): void; /** * Gets statistics about cache usage */ getCacheStats(): { etag: { items: number; byteSize: number; }; content: { items: number; byteSize: number; }; compressedVariants: { items: number; byteSize: number; }; stat: { items: number; byteSize: number; }; }; /** * Updates the static content configuration at runtime with targeted cache * invalidation — only evicting entries whose URL-to-path mapping changed. * * Use this when URL routing is changing but file contents at existing paths * are unchanged (e.g., adding or removing pages without rebuilding assets). * For post-build reloads where file contents may have changed, use * `replaceConfig` instead. * * **Important:** When providing a section, you must provide the COMPLETE mapping for that section. * - If you provide `singleAssetMap`, it replaces the entire single asset map * - If you provide `folderMap`, it replaces the entire folder map * - You can update one section, the other, or both * - Omitted sections remain unchanged * * **Cache invalidation strategy:** * - `singleAssetMap` changes: Only invalidates filesystem paths whose URL-to-path * *mapping* changed (added, removed, or pointed to a different file). Paths whose * mapping is unchanged are not evicted — this method has no visibility into whether * the file content on disk changed. If files were rebuilt in-place, use * `replaceConfig` instead. * - `folderMap` changes: Clears all caches (folder changes are structural) * * @param newConfig Complete mapping(s) for the section(s) you want to update * * @example Update only single asset mappings * ```typescript * cache.updateConfig({ * singleAssetMap: { * '/': './dist/index.html', * '/blog/new-post': './dist/blog/new-post.html' * } * }); * ``` * * @example Update only folder mappings * ```typescript * cache.updateConfig({ * folderMap: { * '/assets': { path: './dist/assets', detectImmutableAssets: true } * } * }); * ``` * * @example Update both sections * ```typescript * cache.updateConfig({ * singleAssetMap: { '/': './dist/index.html' }, * folderMap: { '/assets': './dist/assets' } * }); * ``` */ updateConfig(newConfig: { singleAssetMap?: Record; folderMap?: Record; }): void; /** * Handles an HTTP request by resolving the URL to a file path and serving it * * This is a convenience method that combines URL resolution with file serving. * If no file matches the URL, it returns without sending a response (lets the hook fall through). * * @param rawURL The raw request URL (may include query string or hash) * @param req The Fastify request object * @param reply The Fastify reply object * @returns Information about whether a file was served */ handleRequest(rawURL: string, req: FastifyRequest, reply: FastifyReply): Promise; /** * Normalizes single asset map keys to ensure leading slash * Also validates against null bytes to prevent path injection */ private normalizeSingleAssetMap; /** * Normalizes folder map with proper prefix formatting * Also validates against null bytes to prevent path injection * * Handles two config formats: * 1. String shorthand: { "/assets/": "/path/to/assets" } * 2. Full config object: { "/assets/": { path: "/path/to/assets", detectImmutableAssets: true } } */ private normalizeFolderMap; /** * Normalizes URL prefix: ensures leading and trailing slash, collapses multiple slashes */ private normalizePrefix; /** * Gets the MIME type for a file based on its extension */ private getMimeType; /** * Compares two FolderConfig objects for equality * Dynamically checks all properties so we don't need to update this if FolderConfig changes */ private isSameFolderConfig; /** * Checks if a file appears to be fingerprinted/immutable based on filename * * Detects common build tool fingerprinting patterns: * - .{hash}.{ext} format (e.g., main.a1b2c3d4.js, styles.CTpDmzGw.css) * - -{hash}.{ext} format (e.g., chunk-a1b2c3d4.js, vendor-5f8e9a2b.js) * * Hash must be at least 6 alphanumeric characters * * @param filePath The file path to check * @returns True if the file appears to be fingerprinted */ private isImmutableAsset; private getCompressedCacheKey; private invalidateCompressedVariants; private handleCompressedVariantCacheChange; private untrackCompressedVariantByKey; private untrackCompressedVariant; } /** * 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 = serveSSRDev(paths, { * plugins: [ * staticContent({ * folderMap: { * '/uploads': './uploads', * '/static': './public/static', * }, * }), * ], * }); * ``` * * @example Multiple folders with different settings * ```typescript * import { staticContent } from 'unirend/plugins'; * * const server = serveSSRProd(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 = serveSSRProd(buildDir, { * plugins: [ * staticContent({ * folderMap: { '/uploads': './uploads' }, * }, 'uploads-handler'), * ], * }); * ``` * * @example Use on standalone API server * ```typescript * import { createAPIServer } from 'unirend/server'; * import { staticContent } from 'unirend/plugins'; * * const server = createAPIServer({ * 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, StaticContentCache } from 'unirend/plugins'; * * // Create cache externally for runtime control * const cache = new StaticContentCache({ * folderMap: { '/pages': './dist/pages' } * }); * * const server = serveSSRDev(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 ClientInfoConfig, type CookiesConfig, type DomainValidationConfig, type FolderConfig, type InvalidDomainResponse, type StaticContentRouterOptions, type ValidProductionDomains, clientInfo, cookieUtils, cookies, cors, domainValidation, staticContent };