From f9d1e0c1b2373a92659bd7d098513bf69e0be88c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 19:36:36 +0000 Subject: [PATCH 1/3] feat: harden static-files and vite plugins with security headers, streaming, and config support Static-files plugin: - Use pluginsConfig values (staticPublicDir, staticUploadsDir, staticCacheMaxAge) instead of hardcoded paths - Respect enablePublic / enableUploads config flags to selectively register routes - Add ETag headers with 304 Not Modified support - Add X-Content-Type-Options: nosniff on all responses - Differentiate cache strategy: immutable for hashed assets, short TTL with must-revalidate for uploads - Force Content-Disposition: attachment for dangerous MIME types/extensions Vite plugin: - Serve pre-compressed .gz files when Accept-Encoding includes gzip - Fix SPA fallback to use Cache-Control: no-cache so new deploys are picked up immediately - Stream proxy responses (response.body) instead of buffering with arrayBuffer() - Fix collectFiles to throw explicitly when the build directory is missing instead of silently returning an empty map https://claude.ai/code/session_01R51VjyiwsBiJic3npffRoc --- core/plugins/built-in/vite/index.ts | 91 ++++++++++++--- core/server/plugins/static-files-plugin.ts | 130 +++++++++++++++++---- 2 files changed, 184 insertions(+), 37 deletions(-) diff --git a/core/plugins/built-in/vite/index.ts b/core/plugins/built-in/vite/index.ts index 6948ca6a..b83edf41 100644 --- a/core/plugins/built-in/vite/index.ts +++ b/core/plugins/built-in/vite/index.ts @@ -23,22 +23,35 @@ const HASHED_EXT = /\.[0-9a-f]{8,}\.\w+$/ /** * Recursively collect all files under `dir` as relative paths (e.g. "/assets/app.abc123.js"). * Runs once at startup — in production the build output never changes. + * + * Throws if the directory does not exist so callers get a clear signal + * instead of silently serving nothing. */ function collectFiles(dir: string, prefix = ''): Map { const map = new Map() - try { - const entries = readdirSync(dir, { withFileTypes: true }) - for (const entry of entries) { - const rel = prefix + '/' + entry.name - if (entry.isDirectory()) { - for (const [k, v] of collectFiles(join(dir, entry.name), rel)) { - map.set(k, v) - } - } else if (entry.isFile()) { - map.set(rel, join(dir, entry.name)) + + if (!prefix) { + // Top-level call: verify the directory exists + if (!existsSync(dir)) { + throw new Error( + `Static file directory "${dir}" does not exist. ` + + `Run the client build first or check your configuration.` + ) + } + } + + const entries = readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + const rel = prefix + '/' + entry.name + if (entry.isDirectory()) { + for (const [k, v] of collectFiles(join(dir, entry.name), rel)) { + map.set(k, v) } + } else if (entry.isFile()) { + map.set(rel, join(dir, entry.name)) } - } catch {} + } + return map } @@ -60,6 +73,15 @@ function createStaticFallback() { // Bun.file() handle cache — avoids re-creating handles on repeated requests const fileCache = new Map>() + // Build a set of paths that have a pre-compressed .gz sibling + const gzSet = new Set() + for (const [rel, abs] of fileMap) { + if (rel.endsWith('.gz')) { + // "/assets/app.abc123.js.gz" → "/assets/app.abc123.js" + gzSet.add(rel.slice(0, -3)) + } + } + return (c: { request?: Request }) => { const req = c.request if (!req) return @@ -96,6 +118,39 @@ function createStaticFallback() { // O(1) lookup in pre-scanned file map const absolutePath = fileMap.get(pathname) if (absolutePath) { + // Check for pre-compressed .gz variant + const acceptEncoding = req.headers.get('accept-encoding') || '' + if (gzSet.has(pathname) && acceptEncoding.includes('gzip')) { + const gzPath = pathname + '.gz' + const gzAbsolute = fileMap.get(gzPath) + if (gzAbsolute) { + let gzFile = fileCache.get(gzPath) + if (!gzFile) { + gzFile = Bun.file(gzAbsolute) + fileCache.set(gzPath, gzFile) + } + + // Determine original content type from the uncompressed file + let origFile = fileCache.get(pathname) + if (!origFile) { + origFile = Bun.file(absolutePath) + fileCache.set(pathname, origFile) + } + + const headers: Record = { + 'Content-Encoding': 'gzip', + 'Content-Type': origFile.type || 'application/octet-stream', + 'Vary': 'Accept-Encoding', + } + + if (HASHED_EXT.test(pathname)) { + headers['Cache-Control'] = `public, max-age=${STATIC_MAX_AGE}, immutable` + } + + return new Response(gzFile, { headers }) + } + } + let file = fileCache.get(pathname) if (!file) { file = Bun.file(absolutePath) @@ -114,14 +169,19 @@ function createStaticFallback() { return file } - // SPA fallback: serve index.html for unmatched routes + // SPA fallback: serve index.html for unmatched routes with no-cache + // so the browser always checks for a newer version on deploy if (indexFile) { - return indexFile + return new Response(indexFile, { + headers: { + 'Cache-Control': 'no-cache', + } + }) } } } -/** Proxy request to Vite dev server */ +/** Proxy request to Vite dev server — streams the response body */ async function proxyToVite(ctx: RequestContext): Promise { const { host, port } = clientConfig.vite @@ -143,7 +203,8 @@ async function proxyToVite(ctx: RequestContext): Promise { }) ctx.handled = true - ctx.response = new Response(await response.arrayBuffer(), { + // Stream the response body instead of buffering the entire payload in memory + ctx.response = new Response(response.body, { status: response.status, statusText: response.statusText, headers: response.headers diff --git a/core/server/plugins/static-files-plugin.ts b/core/server/plugins/static-files-plugin.ts index 1288aaaf..99ae2245 100644 --- a/core/server/plugins/static-files-plugin.ts +++ b/core/server/plugins/static-files-plugin.ts @@ -1,9 +1,50 @@ -// 🔥 FluxStack Static Files Plugin - Serve Public Files & Uploads +// FluxStack Static Files Plugin - Serve Public Files & Uploads -import { existsSync, statSync } from 'fs' +import { existsSync, statSync, type Stats } from 'fs' import { mkdir } from 'fs/promises' -import { resolve } from 'path' +import { resolve, extname } from 'path' import type { Plugin, PluginContext } from '../../plugins/types' +import { pluginsConfig } from '@config' + +/** MIME types that should force a download instead of rendering inline */ +const DANGEROUS_MIME_TYPES = new Set([ + 'application/x-msdownload', + 'application/x-executable', + 'application/x-sharedlib', + 'application/x-mach-binary', + 'application/x-dosexec', + 'application/x-httpd-php', + 'application/java-archive', + 'application/x-sh', + 'application/x-csh', + 'application/x-bat', +]) + +/** File extensions that should always force a download */ +const DANGEROUS_EXTENSIONS = new Set([ + '.exe', '.dll', '.bat', '.cmd', '.com', '.msi', + '.sh', '.csh', '.bash', '.ps1', '.vbs', '.wsf', + '.php', '.jsp', '.asp', '.aspx', '.py', '.rb', '.pl', + '.jar', '.war', '.class', + '.scr', '.pif', '.hta', + '.svg', // SVG can contain embedded scripts +]) + +/** Extensions that carry a content hash in their filename (immutable) */ +const HASHED_EXT = /\.[0-9a-f]{8,}\.\w+$/ + +/** Generate an ETag from file stats (size + mtime) */ +function generateETag(stat: Stats): string { + return `"${stat.size.toString(16)}-${stat.mtimeMs.toString(16)}"` +} + +/** Check if a MIME type or extension should force download */ +function shouldForceDownload(filePath: string, mimeType: string | undefined): boolean { + const ext = extname(filePath).toLowerCase() + if (DANGEROUS_EXTENSIONS.has(ext)) return true + if (mimeType && DANGEROUS_MIME_TYPES.has(mimeType)) return true + return false +} export const staticFilesPlugin: Plugin = { name: 'static-files', @@ -14,17 +55,20 @@ export const staticFilesPlugin: Plugin = { tags: ['static', 'files', 'uploads'], setup: async (context: PluginContext) => { - const projectRoot = process.cwd() - const publicDir = resolve(projectRoot, 'public') - const uploadsDir = resolve(projectRoot, 'uploads') + if (!pluginsConfig.staticFilesEnabled) { + context.logger.debug('Static files plugin disabled') + return + } - // Create directories if they don't exist - await mkdir(publicDir, { recursive: true }) - await mkdir(uploadsDir, { recursive: true }) - await mkdir(resolve(uploadsDir, 'avatars'), { recursive: true }) + const projectRoot = process.cwd() + const publicDir = resolve(projectRoot, pluginsConfig.staticPublicDir ?? 'public') + const uploadsDir = resolve(projectRoot, pluginsConfig.staticUploadsDir ?? 'uploads') + const cacheMaxAge = pluginsConfig.staticCacheMaxAge + const enablePublic = pluginsConfig.staticEnablePublic + const enableUploads = pluginsConfig.staticEnableUploads // Helper to serve files from a directory - const serveFile = (baseDir: string) => ({ params, set }: any) => { + const serveFile = (baseDir: string, isUpload: boolean) => ({ params, set, request }: any) => { const requestedPath = params['*'] || '' const filePath = resolve(baseDir, requestedPath) @@ -41,8 +85,10 @@ export const staticFilesPlugin: Plugin = { } // Check if it's a file (not directory) + let stat: ReturnType try { - if (!statSync(filePath).isFile()) { + stat = statSync(filePath) + if (!stat.isFile()) { set.status = 404 return { error: 'Not a file' } } @@ -51,19 +97,59 @@ export const staticFilesPlugin: Plugin = { return { error: 'File not found' } } - // Set cache header (1 year) - set.headers['cache-control'] = 'public, max-age=31536000' + // ETag-based conditional request handling + const etag = generateETag(stat) + const ifNoneMatch = request?.headers?.get?.('if-none-match') + if (ifNoneMatch && ifNoneMatch === etag) { + set.status = 304 + return '' + } + + // Common security headers + set.headers['x-content-type-options'] = 'nosniff' + set.headers['etag'] = etag + + // Cache strategy: hashed assets are immutable, uploads get short cache + if (!isUpload && HASHED_EXT.test(requestedPath)) { + set.headers['cache-control'] = `public, max-age=${cacheMaxAge}, immutable` + } else if (isUpload) { + set.headers['cache-control'] = 'public, max-age=3600, must-revalidate' + } else { + set.headers['cache-control'] = `public, max-age=${cacheMaxAge}` + } + + const file = Bun.file(filePath) + + // Force download for dangerous MIME types + if (shouldForceDownload(filePath, file.type)) { + const fileName = requestedPath.split('/').pop() || 'download' + set.headers['content-disposition'] = `attachment; filename="${fileName}"` + } - // Bun.file() handles: content-type, content-length, streaming - return Bun.file(filePath) + return file } - // Register routes - context.app.get('/api/static/*', serveFile(publicDir)) - context.app.get('/api/uploads/*', serveFile(uploadsDir)) + // Register routes based on config flags + if (enablePublic) { + await mkdir(publicDir, { recursive: true }) + context.app.get('/api/static/*', serveFile(publicDir, false)) + context.logger.debug('Static public files route registered: /api/static/*') + } + + if (enableUploads) { + await mkdir(uploadsDir, { recursive: true }) + await mkdir(resolve(uploadsDir, 'avatars'), { recursive: true }) + context.app.get('/api/uploads/*', serveFile(uploadsDir, true)) + context.logger.debug('Static uploads route registered: /api/uploads/*') + } - context.logger.debug('📁 Static files plugin ready', { - routes: ['/api/static/*', '/api/uploads/*'] - }) + const routes = [ + ...(enablePublic ? ['/api/static/*'] : []), + ...(enableUploads ? ['/api/uploads/*'] : []) + ] + + if (routes.length > 0) { + context.logger.debug('Static files plugin ready', { routes }) + } } } From 1d4674c78c8e4eda5156994e0673b324c4d16a6d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 19:39:38 +0000 Subject: [PATCH 2/3] fix: harden static-files plugin with security and performance improvements - Eliminate double syscall: remove existsSync() before statSync(), use a single statSync() in try-catch - Block null byte injection: reject paths containing \0 early - Add Last-Modified header alongside ETag for full conditional request support (If-Modified-Since) - Use weak ETag (W/"...") since it is derived from stat metadata, not content hash - Return null instead of empty string for 304 responses to avoid unnecessary Content-Length: 0 - Sanitize filenames in Content-Disposition header to prevent header injection from special characters - Use imported Stats type consistently instead of ReturnType - Use path.basename() instead of manual split('/').pop() - Remove hardcoded uploads/avatars directory creation (application concern) https://claude.ai/code/session_01R51VjyiwsBiJic3npffRoc --- core/server/plugins/static-files-plugin.ts | 52 +++++++++++++++------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/core/server/plugins/static-files-plugin.ts b/core/server/plugins/static-files-plugin.ts index 99ae2245..ee5af58a 100644 --- a/core/server/plugins/static-files-plugin.ts +++ b/core/server/plugins/static-files-plugin.ts @@ -1,8 +1,8 @@ // FluxStack Static Files Plugin - Serve Public Files & Uploads -import { existsSync, statSync, type Stats } from 'fs' +import { statSync, type Stats } from 'fs' import { mkdir } from 'fs/promises' -import { resolve, extname } from 'path' +import { resolve, extname, basename } from 'path' import type { Plugin, PluginContext } from '../../plugins/types' import { pluginsConfig } from '@config' @@ -33,9 +33,15 @@ const DANGEROUS_EXTENSIONS = new Set([ /** Extensions that carry a content hash in their filename (immutable) */ const HASHED_EXT = /\.[0-9a-f]{8,}\.\w+$/ -/** Generate an ETag from file stats (size + mtime) */ +/** Generate a weak ETag from file stats (size + mtime) */ function generateETag(stat: Stats): string { - return `"${stat.size.toString(16)}-${stat.mtimeMs.toString(16)}"` + return `W/"${stat.size.toString(16)}-${stat.mtimeMs.toString(16)}"` +} + +/** Sanitize a filename for use in Content-Disposition header */ +function sanitizeFilename(name: string): string { + // Strip path separators, null bytes, and control characters + return name.replace(/[/\\:\0\x01-\x1f\x7f]/g, '_').replace(/"/g, '\\"') } /** Check if a MIME type or extension should force download */ @@ -69,7 +75,14 @@ export const staticFilesPlugin: Plugin = { // Helper to serve files from a directory const serveFile = (baseDir: string, isUpload: boolean) => ({ params, set, request }: any) => { - const requestedPath = params['*'] || '' + const requestedPath: string = params['*'] || '' + + // Reject null bytes early — prevents filesystem confusion + if (requestedPath.includes('\0')) { + set.status = 400 + return { error: 'Invalid path' } + } + const filePath = resolve(baseDir, requestedPath) // Path traversal protection @@ -78,14 +91,8 @@ export const staticFilesPlugin: Plugin = { return { error: 'Invalid path' } } - // Check if file exists - if (!existsSync(filePath)) { - set.status = 404 - return { error: 'File not found' } - } - - // Check if it's a file (not directory) - let stat: ReturnType + // Single syscall: statSync throws if file doesn't exist + let stat: Stats try { stat = statSync(filePath) if (!stat.isFile()) { @@ -97,17 +104,29 @@ export const staticFilesPlugin: Plugin = { return { error: 'File not found' } } - // ETag-based conditional request handling + // ETag / Last-Modified conditional request handling const etag = generateETag(stat) + const lastModified = stat.mtime.toUTCString() + const ifNoneMatch = request?.headers?.get?.('if-none-match') if (ifNoneMatch && ifNoneMatch === etag) { set.status = 304 - return '' + return null + } + + const ifModifiedSince = request?.headers?.get?.('if-modified-since') + if (!ifNoneMatch && ifModifiedSince) { + const clientDate = new Date(ifModifiedSince).getTime() + if (!isNaN(clientDate) && stat.mtimeMs <= clientDate) { + set.status = 304 + return null + } } // Common security headers set.headers['x-content-type-options'] = 'nosniff' set.headers['etag'] = etag + set.headers['last-modified'] = lastModified // Cache strategy: hashed assets are immutable, uploads get short cache if (!isUpload && HASHED_EXT.test(requestedPath)) { @@ -122,7 +141,7 @@ export const staticFilesPlugin: Plugin = { // Force download for dangerous MIME types if (shouldForceDownload(filePath, file.type)) { - const fileName = requestedPath.split('/').pop() || 'download' + const fileName = sanitizeFilename(basename(requestedPath) || 'download') set.headers['content-disposition'] = `attachment; filename="${fileName}"` } @@ -138,7 +157,6 @@ export const staticFilesPlugin: Plugin = { if (enableUploads) { await mkdir(uploadsDir, { recursive: true }) - await mkdir(resolve(uploadsDir, 'avatars'), { recursive: true }) context.app.get('/api/uploads/*', serveFile(uploadsDir, true)) context.logger.debug('Static uploads route registered: /api/uploads/*') } From 44dd388d71ab03a35a765b7e4578a5c6e3e5abe6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 19:49:34 +0000 Subject: [PATCH 3/3] refactor: replace Node fs APIs with Bun-native equivalents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit static-files plugin: - Replace fs.statSync with Bun.file().stat() (async, non-blocking I/O) - Use file.lastModified (Bun-native property) for ETag and Last-Modified instead of importing Stats from fs - Remove fs.statSync and fs.existsSync imports entirely — zero Node fs dependency - Handler is now async, enabling non-blocking file metadata lookups - Bun.file() return still uses sendfile(2) for zero-copy kernel transfer vite plugin: - Replace recursive readdirSync with Bun.Glob("**/*").scanSync() — native C++ glob implementation, no manual recursion - Use Bun.Glob from global (not import from "bun") to stay compatible with Vitest module resolution - Simplify collectFiles from 25 lines of recursive logic to a flat loop - Remove readdirSync and statSync imports https://claude.ai/code/session_01R51VjyiwsBiJic3npffRoc --- core/plugins/built-in/vite/index.ts | 41 +++++++++------------- core/server/plugins/static-files-plugin.ts | 40 ++++++++++++--------- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/core/plugins/built-in/vite/index.ts b/core/plugins/built-in/vite/index.ts index b83edf41..645f9eb5 100644 --- a/core/plugins/built-in/vite/index.ts +++ b/core/plugins/built-in/vite/index.ts @@ -4,7 +4,7 @@ import { clientConfig } from '@config' import { pluginsConfig } from '@config' import { isDevelopment } from "@core/utils/helpers" import { join } from "path" -import { statSync, existsSync, readdirSync } from "fs" +import { existsSync } from "fs" type Plugin = FluxStack.Plugin @@ -21,35 +21,26 @@ const STATIC_MAX_AGE = 31536000 const HASHED_EXT = /\.[0-9a-f]{8,}\.\w+$/ /** - * Recursively collect all files under `dir` as relative paths (e.g. "/assets/app.abc123.js"). - * Runs once at startup — in production the build output never changes. + * Collect all files under `dir` using Bun.Glob (native C++ implementation). + * Returns a map of relative URL paths → absolute filesystem paths. * * Throws if the directory does not exist so callers get a clear signal * instead of silently serving nothing. */ -function collectFiles(dir: string, prefix = ''): Map { - const map = new Map() - - if (!prefix) { - // Top-level call: verify the directory exists - if (!existsSync(dir)) { - throw new Error( - `Static file directory "${dir}" does not exist. ` + - `Run the client build first or check your configuration.` - ) - } +function collectFiles(dir: string): Map { + if (!existsSync(dir)) { + throw new Error( + `Static file directory "${dir}" does not exist. ` + + `Run the client build first or check your configuration.` + ) } - const entries = readdirSync(dir, { withFileTypes: true }) - for (const entry of entries) { - const rel = prefix + '/' + entry.name - if (entry.isDirectory()) { - for (const [k, v] of collectFiles(join(dir, entry.name), rel)) { - map.set(k, v) - } - } else if (entry.isFile()) { - map.set(rel, join(dir, entry.name)) - } + const map = new Map() + const glob = new Bun.Glob("**/*") + + for (const relativePath of glob.scanSync({ cwd: dir, onlyFiles: true, dot: true })) { + // scanSync returns paths like "assets/app.abc123.js" (no leading slash) + map.set('/' + relativePath, join(dir, relativePath)) } return map @@ -75,7 +66,7 @@ function createStaticFallback() { // Build a set of paths that have a pre-compressed .gz sibling const gzSet = new Set() - for (const [rel, abs] of fileMap) { + for (const [rel] of fileMap) { if (rel.endsWith('.gz')) { // "/assets/app.abc123.js.gz" → "/assets/app.abc123.js" gzSet.add(rel.slice(0, -3)) diff --git a/core/server/plugins/static-files-plugin.ts b/core/server/plugins/static-files-plugin.ts index ee5af58a..9721a66b 100644 --- a/core/server/plugins/static-files-plugin.ts +++ b/core/server/plugins/static-files-plugin.ts @@ -1,6 +1,5 @@ // FluxStack Static Files Plugin - Serve Public Files & Uploads -import { statSync, type Stats } from 'fs' import { mkdir } from 'fs/promises' import { resolve, extname, basename } from 'path' import type { Plugin, PluginContext } from '../../plugins/types' @@ -33,14 +32,17 @@ const DANGEROUS_EXTENSIONS = new Set([ /** Extensions that carry a content hash in their filename (immutable) */ const HASHED_EXT = /\.[0-9a-f]{8,}\.\w+$/ -/** Generate a weak ETag from file stats (size + mtime) */ -function generateETag(stat: Stats): string { - return `W/"${stat.size.toString(16)}-${stat.mtimeMs.toString(16)}"` +/** + * Generate a weak ETag from Bun.file() metadata. + * Uses file.lastModified (ms timestamp) which is available without reading the file. + * Size comes from stat() since BunFile.size is unreliable until contents are read. + */ +function generateETag(size: number, lastModified: number): string { + return `W/"${size.toString(16)}-${lastModified.toString(16)}"` } /** Sanitize a filename for use in Content-Disposition header */ function sanitizeFilename(name: string): string { - // Strip path separators, null bytes, and control characters return name.replace(/[/\\:\0\x01-\x1f\x7f]/g, '_').replace(/"/g, '\\"') } @@ -73,8 +75,8 @@ export const staticFilesPlugin: Plugin = { const enablePublic = pluginsConfig.staticEnablePublic const enableUploads = pluginsConfig.staticEnableUploads - // Helper to serve files from a directory - const serveFile = (baseDir: string, isUpload: boolean) => ({ params, set, request }: any) => { + // Async handler — uses Bun.file() APIs instead of Node fs + const serveFile = (baseDir: string, isUpload: boolean) => async ({ params, set, request }: any) => { const requestedPath: string = params['*'] || '' // Reject null bytes early — prevents filesystem confusion @@ -91,10 +93,13 @@ export const staticFilesPlugin: Plugin = { return { error: 'Invalid path' } } - // Single syscall: statSync throws if file doesn't exist - let stat: Stats + const file = Bun.file(filePath) + + // Bun.file().stat() — single async call, no Node fs import needed. + // Returns size, isFile(), ctime etc. without reading file contents. + let stat: Awaited> try { - stat = statSync(filePath) + stat = await file.stat() if (!stat.isFile()) { set.status = 404 return { error: 'Not a file' } @@ -104,10 +109,11 @@ export const staticFilesPlugin: Plugin = { return { error: 'File not found' } } - // ETag / Last-Modified conditional request handling - const etag = generateETag(stat) - const lastModified = stat.mtime.toUTCString() + // ETag from stat.size (reliable) + file.lastModified (Bun-native, no extra syscall) + const etag = generateETag(stat.size, file.lastModified) + const lastModified = new Date(file.lastModified).toUTCString() + // Conditional request: If-None-Match takes priority over If-Modified-Since const ifNoneMatch = request?.headers?.get?.('if-none-match') if (ifNoneMatch && ifNoneMatch === etag) { set.status = 304 @@ -117,13 +123,13 @@ export const staticFilesPlugin: Plugin = { const ifModifiedSince = request?.headers?.get?.('if-modified-since') if (!ifNoneMatch && ifModifiedSince) { const clientDate = new Date(ifModifiedSince).getTime() - if (!isNaN(clientDate) && stat.mtimeMs <= clientDate) { + if (!isNaN(clientDate) && file.lastModified <= clientDate) { set.status = 304 return null } } - // Common security headers + // Security headers set.headers['x-content-type-options'] = 'nosniff' set.headers['etag'] = etag set.headers['last-modified'] = lastModified @@ -137,14 +143,14 @@ export const staticFilesPlugin: Plugin = { set.headers['cache-control'] = `public, max-age=${cacheMaxAge}` } - const file = Bun.file(filePath) - // Force download for dangerous MIME types + // file.type is resolved from extension by Bun — no disk I/O if (shouldForceDownload(filePath, file.type)) { const fileName = sanitizeFilename(basename(requestedPath) || 'download') set.headers['content-disposition'] = `attachment; filename="${fileName}"` } + // Returning Bun.file() directly lets Bun use sendfile(2) for zero-copy transfer return file }