/** * Hono Application Setup * * Configures Hono app with CORS, security headers, and routes. */ import { Hono } from 'hono'; import type { Context } from 'hono'; import type { Env } from './types'; import { CORS, HTTP_STATUS } from './constants'; import { createLogger } from './utils/logger'; import { requestIdMiddleware, authMiddleware, rateLimitMiddleware, optionalAuthMiddleware, } from './middleware/hono-adapters'; import { handleHealth } from './routes/health'; import { handleInstances } from './routes/instances'; import { handleSync } from './routes/sync'; const logger = createLogger('[App]'); // Context variables type type Variables = { requestId: string; authenticated?: boolean; }; // Create Hono app with type-safe bindings const app = new Hono<{ Bindings: Env; Variables: Variables }>(); /** * Get CORS origin for request * Reused from original index.ts logic */ function getCorsOrigin(c: Context<{ Bindings: Env; Variables: Variables }>): string { const origin = c.req.header('Origin'); const env = c.env; // Environment variable has explicit origin configured (highest priority) if (env.CORS_ORIGIN && env.CORS_ORIGIN !== '*') { return env.CORS_ORIGIN; } // Build allowed origins list based on environment const isDevelopment = env.ENVIRONMENT === 'development'; const allowedOrigins = isDevelopment ? [...CORS.ALLOWED_ORIGINS, ...CORS.DEVELOPMENT_ORIGINS] : CORS.ALLOWED_ORIGINS; // Request origin is in allowed list if (origin && allowedOrigins.includes(origin)) { return origin; } // Log unmatched origins for security monitoring if (origin && !allowedOrigins.includes(origin)) { const sanitizedOrigin = origin.replace(/[\r\n\t]/g, '').substring(0, 256); logger.warn('Unmatched origin - using default', { requested_origin: sanitizedOrigin, environment: env.ENVIRONMENT || 'production', default_origin: CORS.DEFAULT_ORIGIN, }); } // Return explicit default (no wildcard) return CORS.DEFAULT_ORIGIN; } /** * CORS middleware * Configured dynamically based on request origin */ app.use('*', async (c, next) => { // Handle OPTIONS preflight - must come before await next() if (c.req.method === 'OPTIONS') { const origin = getCorsOrigin(c); c.res.headers.set('Access-Control-Allow-Origin', origin); c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); c.res.headers.set('Access-Control-Allow-Headers', 'Content-Type, X-API-Key'); c.res.headers.set('Access-Control-Max-Age', CORS.MAX_AGE); return c.body(null, 204); } await next(); // Set CORS headers after processing const origin = getCorsOrigin(c); c.res.headers.set('Access-Control-Allow-Origin', origin); c.res.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); c.res.headers.set('Access-Control-Allow-Headers', 'Content-Type, X-API-Key'); c.res.headers.set('Access-Control-Max-Age', CORS.MAX_AGE); c.res.headers.set( 'Access-Control-Expose-Headers', 'X-RateLimit-Retry-After, Retry-After, X-Request-ID' ); }); /** * Request ID middleware * Adds unique request ID for tracing */ app.use('*', requestIdMiddleware); /** * Security headers middleware * Applied to all responses */ app.use('*', async (c, next) => { await next(); // Add security headers to response c.res.headers.set('X-Content-Type-Options', 'nosniff'); c.res.headers.set('X-Frame-Options', 'DENY'); c.res.headers.set('Strict-Transport-Security', 'max-age=31536000'); c.res.headers.set('Content-Security-Policy', "default-src 'none'"); c.res.headers.set('X-XSS-Protection', '1; mode=block'); c.res.headers.set('Referrer-Policy', 'no-referrer'); }); /** * Environment validation middleware * Checks required environment variables before processing */ app.use('*', async (c, next) => { const required = ['API_KEY']; const missing = required.filter((key) => !c.env[key as keyof Env]); if (missing.length > 0) { logger.error('Missing required environment variables', { missing, request_id: c.get('requestId'), }); return c.json( { error: 'Service Unavailable', message: 'Service configuration error', }, 503 ); } return next(); }); /** * Routes */ // Health check (public endpoint with optional authentication) app.get('/health', optionalAuthMiddleware, handleHealth); // Query instances (authenticated, rate limited) app.get('/instances', authMiddleware, rateLimitMiddleware, handleInstances); // Sync trigger (authenticated, rate limited) app.post('/sync', authMiddleware, rateLimitMiddleware, handleSync); /** * 404 handler */ app.notFound((c) => { return c.json( { error: 'Not Found', path: c.req.path, }, HTTP_STATUS.NOT_FOUND ); }); /** * Global error handler */ app.onError((err, c) => { logger.error('Request error', { error: err, request_id: c.get('requestId'), }); return c.json( { error: 'Internal Server Error', }, HTTP_STATUS.INTERNAL_ERROR ); }); export default app;