/** * Tractatus Express Server * Main application entry point */ require('dotenv').config(); const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const rateLimit = require('express-rate-limit'); const cookieParser = require('cookie-parser'); // const csrf = require('csurf'); // Disabled - deprecated package, will implement modern solution in Phase 3 const config = require('./config/app.config'); const logger = require('./utils/logger.util'); const { connect: connectDb, close: closeDb } = require('./utils/db.util'); const { connect: connectMongoose, close: closeMongoose } = require('./utils/mongoose.util'); const { notFound, errorHandler } = require('./middleware/error.middleware'); // Security middleware (Quick Wins) const { securityHeadersMiddleware } = require('./middleware/security-headers.middleware'); const { publicRateLimiter, formRateLimiter, authRateLimiter } = require('./middleware/rate-limit.middleware'); const { sanitizeErrorResponse, sanitizeResponseData } = require('./middleware/response-sanitization.middleware'); const { setCsrfToken, csrfProtection, getCsrfToken } = require('./middleware/csrf-protection.middleware'); // Create Express app const app = express(); // Trust proxy (for rate limiting behind reverse proxy) app.set('trust proxy', 1); // ============================================================ // SECURITY MIDDLEWARE (Quick Wins - inst_041-046) // ============================================================ // Enhanced security headers (replaces helmet CSP with more specific policy) app.use(securityHeadersMiddleware); // Keep helmet for other security features (but CSP already set above) app.use(helmet({ contentSecurityPolicy: false // Disabled - using our custom CSP in securityHeadersMiddleware })); // CORS app.use(cors(config.cors)); // Cookie parser (required for CSRF) app.use(cookieParser()); // Set CSRF token cookie on all requests app.use(setCsrfToken); // Response data sanitization (removes sensitive fields) app.use(sanitizeResponseData); // Raw body capture for Stripe webhooks (must be before JSON parser) app.use('/api/koha/webhook', express.raw({ type: 'application/json' }), (req, res, next) => { req.rawBody = req.body; next(); }); // Body parsers (reduced limit from 10mb to 1mb for security) app.use(express.json({ limit: '1mb' })); app.use(express.urlencoded({ extended: true, limit: '1mb' })); // Request logging app.use(logger.request); // Analytics tracking (privacy-respecting, self-hosted) const { trackPageView } = require('./middleware/analytics.middleware'); app.use(trackPageView); // CSRF Protection (Modern Implementation - Phase 0 Complete) // Uses SameSite cookies + double-submit cookie pattern // Protection is applied selectively to state-changing routes (POST, PUT, DELETE, PATCH) // Webhooks and public endpoints are excluded // Enhanced rate limiting (Quick Wins) // Public endpoints: 100 requests per 15 minutes per IP app.use(publicRateLimiter); // Cache control middleware for static assets app.use((req, res, next) => { const path = req.path; // Version manifest and service worker: No cache (always fetch fresh) if (path === '/version.json' || path === '/service-worker.js') { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } // HTML files: No cache (always fetch fresh - users must see updates immediately) else if (path.endsWith('.html') || path === '/') { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); res.setHeader('Pragma', 'no-cache'); } // Admin JS/HTML files: NEVER cache (always fetch fresh) else if (path.startsWith('/js/admin/') || path.startsWith('/admin/')) { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } // CSS and JS files: Short cache for active development // With versioned URLs (?v=timestamp), browsers will fetch new versions when HTML updates else if (path.endsWith('.css') || path.endsWith('.js')) { res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour - reasonable for active development } // Images and fonts: Long cache else if (path.match(/\.(jpg|jpeg|png|gif|svg|ico|woff|woff2|ttf|eot)$/)) { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); // 1 year } // PWA manifest: Medium cache else if (path === '/manifest.json') { res.setHeader('Cache-Control', 'public, max-age=86400'); // 1 day } // Everything else: Short cache else { res.setHeader('Cache-Control', 'public, max-age=3600'); // 1 hour } next(); }); // Blog post URL rewriting: /blog/:slug → /blog-post.html?slug=:slug // This provides cleaner URLs for blog posts (SEO-friendly) app.get('/blog/:slug', (req, res) => { res.redirect(301, `/blog-post.html?slug=${req.params.slug}`); }); // Static files app.use(express.static('public')); // Health check endpoint (minimal, no sensitive data) app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // CSRF token endpoint (modern implementation) // Returns the CSRF token from cookie for client-side usage app.get('/api/csrf-token', getCsrfToken); // API routes const apiRoutes = require('./routes/index'); app.use('/api', apiRoutes); // Homepage (temporary) app.get('/', (req, res) => { res.send(`
✓ Server Running
Development environment for the Tractatus-Based LLM Safety Framework website.
GET /health - Health checkGET /api - API documentationPOST /api/auth/login - Admin loginGET /api/documents - List framework documentsGET /api/blog - List published blog postsGET /api/admin/stats - System statistics (auth required)Phase 1 Development - Not for public use
`); }); // ============================================================ // ERROR HANDLING (Quick Wins) // ============================================================ // 404 handler app.use(notFound); // Enhanced error handler (sanitizes responses, hides stack traces) app.use(sanitizeErrorResponse); // Fallback to original error handler if needed app.use(errorHandler); // Server startup async function start() { try { // Connect to MongoDB (native driver) await connectDb(); // Connect Mongoose (for ODM models) await connectMongoose(); // Sync instructions from file to database try { const { syncInstructions } = require('../scripts/sync-instructions-to-db.js'); const syncResult = await syncInstructions({ silent: true }); if (syncResult && syncResult.success) { logger.info(`✅ Instructions synced to database: ${syncResult.finalCount} active rules`); if (syncResult.added > 0 || syncResult.deactivated > 0) { logger.info(` Added: ${syncResult.added}, Updated: ${syncResult.updated}, Deactivated: ${syncResult.deactivated}`); } } } catch (err) { logger.warn(`⚠️ Instruction sync failed: ${err.message}`); logger.warn(' Admin UI may show outdated rule counts'); } // Initialize governance services const BoundaryEnforcer = require('./services/BoundaryEnforcer.service'); await BoundaryEnforcer.initialize(); const PluralisticDeliberationOrchestrator = require('./services/PluralisticDeliberationOrchestrator.service'); await PluralisticDeliberationOrchestrator.initialize(); const InstructionPersistenceClassifier = require('./services/InstructionPersistenceClassifier.service'); await InstructionPersistenceClassifier.initialize(); const MetacognitiveVerifier = require('./services/MetacognitiveVerifier.service'); await MetacognitiveVerifier.initialize(); const CrossReferenceValidator = require('./services/CrossReferenceValidator.service'); await CrossReferenceValidator.initialize(); const ContextPressureMonitor = require('./services/ContextPressureMonitor.service'); await ContextPressureMonitor.initialize(); logger.info('✅ Governance services initialized (6 core services)'); // Start server const server = app.listen(config.port, () => { logger.info(`🚀 Tractatus server started`); logger.info(`✅ Environment: ${config.env}`); logger.info(`✅ Port: ${config.port}`); logger.info(`✅ MongoDB: ${config.mongodb.db}`); logger.info(`🔒 Security: Quick Wins active (headers, rate limiting, input validation)`); logger.info(`📊 Security logs: ${process.env.HOME}/var/log/tractatus/security-audit.log`); logger.info(`✨ Ready for development`); console.log(`\n🌐 http://localhost:${config.port}\n`); }); // Graceful shutdown process.on('SIGTERM', () => shutdown(server)); process.on('SIGINT', () => shutdown(server)); } catch (error) { logger.error('Failed to start server:', error); process.exit(1); } } // Graceful shutdown async function shutdown(server) { logger.info('Shutting down gracefully...'); server.close(async () => { logger.info('HTTP server closed'); await closeDb(); logger.info('Native MongoDB connection closed'); await closeMongoose(); logger.info('Mongoose connection closed'); process.exit(0); }); // Force shutdown after 10 seconds setTimeout(() => { logger.error('Forced shutdown after timeout'); process.exit(1); }, 10000); } // Start server if run directly if (require.main === module) { start(); } module.exports = app;