3333// FOIA Stream - Main Application Entry
3434// ============================================
3535
36+ import { bodyLimit } from 'hono/body-limit' ;
37+ import { contextStorage } from 'hono/context-storage' ;
3638import { cors } from 'hono/cors' ;
3739import { prettyJSON } from 'hono/pretty-json' ;
3840import { secureHeaders } from 'hono/secure-headers' ;
41+ import { timing } from 'hono/timing' ;
3942
4043import { env } from './config/env' ;
4144import configureOpenAPI from './lib/configure-open-api' ;
4245import createApp from './lib/create-app' ;
46+ import { logger } from './lib/logger' ;
47+ import {
48+ apiRateLimit ,
49+ authRateLimit ,
50+ autoBanProtection ,
51+ passwordResetRateLimit ,
52+ uploadRateLimit ,
53+ } from './middleware/rate-limit.middleware' ;
4354import { httpsEnforcement , requestId } from './middleware/security.middleware' ;
4455import { cacheMiddleware } from './middleware/shared.middleware' ;
4556// OpenAPI Routes (all modules now use OpenAPI pattern)
@@ -51,6 +62,21 @@ import redactionOpenAPIRoute from './routes/redaction';
5162import requestsOpenAPIRoute from './routes/requests' ;
5263import templatesOpenAPIRoute from './routes/templates' ;
5364
65+ // ============================================
66+ // Timing Middleware Configuration
67+ // ============================================
68+
69+ /**
70+ * Server-Timing header for performance monitoring
71+ * @compliance NIST 800-53 AU-12 (Audit Generation)
72+ */
73+ const timingMiddleware = timing ( {
74+ total : true ,
75+ enabled : env . NODE_ENV === 'production' ,
76+ totalDescription : 'Total API Response Time' ,
77+ autoEnd : true ,
78+ } ) ;
79+
5480/**
5581 * Main Hono application instance with OpenAPI support
5682 *
@@ -63,27 +89,150 @@ import templatesOpenAPIRoute from './routes/templates';
6389const app = createApp ( ) ;
6490
6591// ============================================
66- // Additional Global Middleware
92+ // Core Middleware Stack
6793// ============================================
6894
95+ // Context storage for request-scoped data
96+ app . use ( '*' , contextStorage ( ) ) ;
97+
98+ // Performance timing headers
99+ app . use ( '*' , timingMiddleware ) ;
100+
69101// Request ID for tracing
70102app . use ( '*' , requestId ( ) ) ;
71103
104+ // Body size limit (2MB default)
105+ // @compliance NIST 800-53 SI-10 (Information Input Validation)
106+ app . use ( '*' , bodyLimit ( { maxSize : env . MAX_FILE_SIZE || 1024 * 1024 * 2 } ) ) ;
107+
72108// HTTPS enforcement (redirects HTTP to HTTPS in production)
73109// @compliance NIST 800-53 SC-8 (Transmission Confidentiality)
74110app . use ( '*' , httpsEnforcement ( ) ) ;
75111
76- // Security headers
77- app . use ( '*' , secureHeaders ( ) ) ;
112+ // Enhanced security headers
113+ // @compliance NIST 800-53 SC-8, SC-13 (Cryptographic Protection)
114+ // @compliance OWASP Security Headers
115+ app . use (
116+ '*' ,
117+ secureHeaders ( {
118+ // HSTS: 2 years with subdomains and preload (max security)
119+ strictTransportSecurity : 'max-age=63072000; includeSubDomains; preload' ,
120+
121+ // Prevent clickjacking completely
122+ xFrameOptions : 'DENY' ,
123+
124+ // XSS Protection: Modern browsers use CSP, but set to 0 to prevent bypass attacks
125+ // (mode=block can introduce vulnerabilities in older IE)
126+ xXssProtection : '0' ,
127+
128+ // Prevent MIME type sniffing attacks
129+ xContentTypeOptions : 'nosniff' ,
130+
131+ // Disable DNS prefetching to prevent information leakage
132+ xDnsPrefetchControl : 'off' ,
133+
134+ // Prevent IE from opening downloads directly
135+ xDownloadOptions : 'noopen' ,
136+
137+ // Block Flash/PDF cross-domain policies
138+ xPermittedCrossDomainPolicies : 'none' ,
139+
140+ // Cross-Origin Isolation headers for maximum security
141+ crossOriginEmbedderPolicy : 'require-corp' ,
142+ crossOriginResourcePolicy : 'same-origin' ,
143+ crossOriginOpenerPolicy : 'same-origin' ,
144+
145+ // Enable origin isolation for performance and security
146+ originAgentCluster : true ,
147+
148+ // Strict referrer policy - only send origin for same-origin, nothing for cross-origin
149+ referrerPolicy : 'strict-origin-when-cross-origin' ,
150+
151+ // Content Security Policy - tight restrictions
152+ contentSecurityPolicy : {
153+ defaultSrc : [ "'self'" ] ,
154+ // Scripts: self only, no inline scripts (use nonces in production)
155+ scriptSrc : [ "'self'" ] ,
156+ // Styles: self only, inline needed for some UI frameworks
157+ styleSrc : [ "'self'" , "'unsafe-inline'" ] ,
158+ // Images: self, data URIs for base64, and HTTPS sources
159+ imgSrc : [ "'self'" , 'data:' , 'https:' ] ,
160+ // Connections: same origin plus the API domain
161+ connectSrc : [ "'self'" ] ,
162+ // Fonts: self and data URIs
163+ fontSrc : [ "'self'" , 'data:' ] ,
164+ // Block all object/embed/applet elements
165+ objectSrc : [ "'none'" ] ,
166+ // Media: self only
167+ mediaSrc : [ "'self'" ] ,
168+ // Frames: none - prevent embedding
169+ frameSrc : [ "'none'" ] ,
170+ // Frame ancestors: none - prevent being embedded
171+ frameAncestors : [ "'none'" ] ,
172+ // Base URI: self only - prevent base tag hijacking
173+ baseUri : [ "'self'" ] ,
174+ // Form actions: self only
175+ formAction : [ "'self'" ] ,
176+ // Upgrade insecure requests to HTTPS
177+ upgradeInsecureRequests : [ ] ,
178+ } ,
179+
180+ // Permissions Policy - disable ALL unnecessary browser features
181+ // This prevents fingerprinting and limits attack surface
182+ permissionsPolicy : {
183+ // Location/sensors - disable completely
184+ accelerometer : [ ] ,
185+ ambientLightSensor : [ ] ,
186+ autoplay : [ ] ,
187+ battery : [ ] ,
188+ camera : [ ] ,
189+ displayCapture : [ ] ,
190+ encryptedMedia : [ ] ,
191+ executionWhileNotRendered : [ ] ,
192+ executionWhileOutOfViewport : [ ] ,
193+ fullscreen : [ "'self'" ] ,
194+ gamepad : [ ] ,
195+ geolocation : [ ] ,
196+ gyroscope : [ ] ,
197+ hid : [ ] ,
198+ identityCredentialsGet : [ ] ,
199+ idleDetection : [ ] ,
200+ localFonts : [ ] ,
201+ magnetometer : [ ] ,
202+ microphone : [ ] ,
203+ midi : [ ] ,
204+ payment : [ ] ,
205+ pictureInPicture : [ ] ,
206+ publickeyCredentialsGet : [ ] ,
207+ screenWakeLock : [ ] ,
208+ serial : [ ] ,
209+ speakerSelection : [ ] ,
210+ // Only enable storage access for self
211+ storageAccess : [ "'self'" ] ,
212+ syncXhr : [ ] ,
213+ usb : [ ] ,
214+ webShare : [ ] ,
215+ windowManagement : [ ] ,
216+ xrSpatialTracking : [ ] ,
217+ } ,
218+ } ) ,
219+ ) ;
78220
79221// CORS
80222app . use (
81223 '*' ,
82224 cors ( {
83225 origin : env . CORS_ORIGIN === '*' ? '*' : env . CORS_ORIGIN . split ( ',' ) ,
84- allowMethods : [ 'GET' , 'POST' , 'PUT' , 'PATCH' , 'DELETE' , 'OPTIONS' ] ,
85- allowHeaders : [ 'Content-Type' , 'Authorization' , 'b3' , 'traceparent' , 'tracestate' ] ,
86- exposeHeaders : [ 'Content-Length' , 'X-Request-Id' ] ,
226+ allowMethods : [ 'GET' , 'POST' , 'PUT' , 'PATCH' , 'DELETE' , 'OPTIONS' , 'HEAD' ] ,
227+ allowHeaders : [
228+ 'Content-Type' ,
229+ 'Authorization' ,
230+ 'b3' ,
231+ 'traceparent' ,
232+ 'tracestate' ,
233+ 'X-Request-Id' ,
234+ ] ,
235+ exposeHeaders : [ 'Content-Length' , 'X-Request-Id' , 'Server-Timing' ] ,
87236 maxAge : 86400 ,
88237 credentials : true ,
89238 } ) ,
@@ -94,6 +243,30 @@ if (env.NODE_ENV === 'development') {
94243 app . use ( '*' , prettyJSON ( ) ) ;
95244}
96245
246+ // ============================================
247+ // Global Error Handler
248+ // ============================================
249+
250+ /**
251+ * Global error handler for unhandled exceptions
252+ * @compliance NIST 800-53 SI-11 (Error Handling)
253+ */
254+ app . onError ( ( err , c ) => {
255+ const error = err instanceof Error ? err : new Error ( String ( err ) ) ;
256+
257+ logger . error ( { error : error . message , stack : error . stack } , 'Unhandled API error' ) ;
258+
259+ const errorResponse = {
260+ success : false ,
261+ error : {
262+ message : env . NODE_ENV === 'production' ? 'Internal server error' : error . message ,
263+ code : error . name || 'UNKNOWN_ERROR' ,
264+ } ,
265+ } ;
266+
267+ return c . json ( errorResponse , 500 ) ;
268+ } ) ;
269+
97270// ============================================
98271// Health Check (inline for OpenAPI spec)
99272// ============================================
@@ -154,6 +327,30 @@ app.use('/api/v1/requests/*', cacheMiddleware('Private'));
154327app . use ( '/api/v1/documents/*' , cacheMiddleware ( 'Private' ) ) ;
155328app . use ( '/api/v1/redaction/*' , cacheMiddleware ( 'NoCache' ) ) ;
156329
330+ // ============================================
331+ // Rate Limiting
332+ // ============================================
333+ // @compliance NIST 800-53 SC-5 (Denial of Service Protection)
334+
335+ // Global API rate limit (100 requests/minute)
336+ app . use ( '/api/v1/*' , apiRateLimit ) ;
337+
338+ // Auto-ban protection - bans IPs after repeated rate limit violations
339+ app . use ( '/api/v1/*' , autoBanProtection ( ) ) ;
340+
341+ // Strict rate limits for authentication endpoints (5 requests/15 min)
342+ // Prevents brute-force password guessing attacks
343+ app . use ( '/api/v1/auth/login' , authRateLimit ) ;
344+ app . use ( '/api/v1/auth/register' , authRateLimit ) ;
345+
346+ // Very strict limit for password reset (3 requests/hour)
347+ app . use ( '/api/v1/auth/forgot-password' , passwordResetRateLimit ) ;
348+ app . use ( '/api/v1/auth/reset-password' , passwordResetRateLimit ) ;
349+
350+ // Upload rate limit for document endpoints (20 uploads/hour)
351+ app . use ( '/api/v1/documents/upload' , uploadRateLimit ) ;
352+ app . use ( '/api/v1/redaction/upload' , uploadRateLimit ) ;
353+
157354app . route ( '/' , indexRoute ) ;
158355app . route ( '/api/v1' , authOpenAPIRoute ) ;
159356app . route ( '/api/v1' , agenciesOpenAPIRoute ) ;
0 commit comments