/** * Tractatus Express Server * Main application entry point */ require('dotenv').config(); const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); 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 } = require('./middleware/rate-limit.middleware'); const { sanitizeErrorResponse, sanitizeResponseData } = require('./middleware/response-sanitization.middleware'); const { setCsrfToken, 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; if (path === '/version.json' || path === '/service-worker.js') { // Version manifest and service worker: never cache (always fetch fresh) res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } else if (path.endsWith('.html') || path === '/') { // HTML: never cache — users must see updates immediately res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); res.setHeader('Pragma', 'no-cache'); } else if (path.startsWith('/js/admin/') || path.startsWith('/admin/')) { // Admin JS/HTML: never cache (always fetch fresh) res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } else if (path.endsWith('.css') || path.endsWith('.js')) { // CSS/JS: 1 hour — versioned URLs force fresh fetch when HTML updates res.setHeader('Cache-Control', 'public, max-age=3600'); } else if (path.match(/\.(jpg|jpeg|png|gif|svg|ico|woff|woff2|ttf|eot)$/)) { // Images/fonts: 1 year, immutable res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } else if (path === '/manifest.json') { // PWA manifest: 1 day res.setHeader('Cache-Control', 'public, max-age=86400'); } else if (path.startsWith('/api/')) { // API: never cache (security middleware sets this too; catch-all below would otherwise override) res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } else { // Static assets without extension: 1 hour res.setHeader('Cache-Control', 'public, max-age=3600'); } next(); }); // Blog index: /blog → blog.html (before /blog/:slug so "/blog" doesn't get treated as a slug) app.get('/blog', (req, res) => { res.sendFile('blog.html', { root: 'public' }); }); // 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}`); }); // RSS feeds (mounted at root level for standard RSS convention) const rssRoutes = require('./routes/rss.routes'); app.use('/', rssRoutes); // 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(` Tractatus AI Safety Framework

Tractatus AI Safety Framework

✓ Server Running

Development environment for the Tractatus-Based LLM Safety Framework website.

Status

Available Endpoints

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`); // Scheduled: check for unnotified published posts every hour try { const blogNotifier = require('./services/blogpost-notifier.service'); setInterval(() => { blogNotifier.checkAndNotify().catch(err => { logger.error('[Scheduled] Blog notifier check failed:', err.message); }); }, 60 * 60 * 1000); // Every hour logger.info('📬 Blog post subscriber notifications: active (hourly check)'); } catch (err) { logger.warn('📬 Blog notifier not available:', err.message); } }); // 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;