From 4c656385fe5829636cf3416e91d2160d05cb058d Mon Sep 17 00:00:00 2001 From: TheFlow Date: Thu, 23 Oct 2025 10:57:20 +1300 Subject: [PATCH] feat(server): add security middleware and website-specific routes Server Infrastructure Updates: - Added response sanitization middleware (fixes Date serialization) - Added CSRF protection middleware (double-submit cookie pattern) - Enhanced rate limiting (public, form, auth limiters) - Added cache control middleware for static assets - Added cookie parser for CSRF support Route Organization: - Reorganized routes for website (auth, documents, blog, newsletter) - Separated admin routes with /admin prefix - Added koha routes for donations - Added demo routes for interactive demonstrations - Dev/test routes only in development environment Config Updates: - Updated app config for website platform - Added website-specific configuration options Model Updates: - Updated model exports for website collections - Added blog, media, newsletter models These changes support the website platform while maintaining the underlying Tractatus governance framework. --- src/config/app.config.js | 21 ++++- src/models/index.js | 34 +++---- src/routes/index.js | 164 +++++++++++++++++++++++--------- src/server.js | 199 +++++++++++++++++++++++++++++++-------- 4 files changed, 317 insertions(+), 101 deletions(-) diff --git a/src/config/app.config.js b/src/config/app.config.js index d1ed32e4..11478efa 100644 --- a/src/config/app.config.js +++ b/src/config/app.config.js @@ -1,13 +1,12 @@ /** * Application Configuration - * Generic configuration template for Tractatus Framework implementations */ module.exports = { // Server port: process.env.PORT || 9000, env: process.env.NODE_ENV || 'development', - appName: process.env.APP_NAME || 'Tractatus Framework', + appName: process.env.APP_NAME || 'Tractatus', // MongoDB mongodb: { @@ -15,12 +14,30 @@ module.exports = { db: process.env.MONGODB_DB || 'tractatus_dev' }, + // JWT + jwt: { + secret: process.env.JWT_SECRET || 'CHANGE_THIS_IN_PRODUCTION', + expiry: process.env.JWT_EXPIRY || '7d' + }, + + // Admin + admin: { + email: process.env.ADMIN_EMAIL || 'john.stroh.nz@pm.me' + }, + // Logging logging: { level: process.env.LOG_LEVEL || 'info', file: process.env.LOG_FILE || 'logs/app.log' }, + // Feature Flags + features: { + aiCuration: process.env.ENABLE_AI_CURATION === 'true', + mediaTriage: process.env.ENABLE_MEDIA_TRIAGE === 'true', + caseSubmissions: process.env.ENABLE_CASE_SUBMISSIONS === 'true' + }, + // Security security: { rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 min diff --git a/src/models/index.js b/src/models/index.js index 9ef5a549..7e01bb50 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -1,26 +1,28 @@ /** * Models Index - * Export all Tractatus Framework models + * Export all models */ -const AuditLog = require('./AuditLog.model'); -const DeliberationSession = require('./DeliberationSession.model'); +const Document = require('./Document.model'); +const BlogPost = require('./BlogPost.model'); +const MediaInquiry = require('./MediaInquiry.model'); +const CaseSubmission = require('./CaseSubmission.model'); +const Resource = require('./Resource.model'); +const ModerationQueue = require('./ModerationQueue.model'); +const User = require('./User.model'); const GovernanceLog = require('./GovernanceLog.model'); -const GovernanceRule = require('./GovernanceRule.model'); +const DeliberationSession = require('./DeliberationSession.model'); const Precedent = require('./Precedent.model'); -const Project = require('./Project.model'); -const SessionState = require('./SessionState.model'); -const VariableValue = require('./VariableValue.model'); -const VerificationLog = require('./VerificationLog.model'); module.exports = { - AuditLog, - DeliberationSession, + Document, + BlogPost, + MediaInquiry, + CaseSubmission, + Resource, + ModerationQueue, + User, GovernanceLog, - GovernanceRule, - Precedent, - Project, - SessionState, - VariableValue, - VerificationLog + DeliberationSession, + Precedent }; diff --git a/src/routes/index.js b/src/routes/index.js index 903e86c3..e1570869 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,71 +1,147 @@ /** * Routes Index - * Central routing configuration for Tractatus Framework API + * Central routing configuration */ const express = require('express'); const router = express.Router(); -// Import framework route modules +// Import route modules +const authRoutes = require('./auth.routes'); +const documentsRoutes = require('./documents.routes'); +const blogRoutes = require('./blog.routes'); +const newsletterRoutes = require('./newsletter.routes'); +const mediaRoutes = require('./media.routes'); +const casesRoutes = require('./cases.routes'); +const adminRoutes = require('./admin.routes'); +const hooksMetricsRoutes = require('./hooks-metrics.routes'); +const syncHealthRoutes = require('./sync-health.routes'); const rulesRoutes = require('./rules.routes'); const projectsRoutes = require('./projects.routes'); const auditRoutes = require('./audit.routes'); const governanceRoutes = require('./governance.routes'); +const kohaRoutes = require('./koha.routes'); +const demoRoutes = require('./demo.routes'); -// Mount framework routes -router.use('/rules', rulesRoutes); -router.use('/projects', projectsRoutes); -router.use('/audit', auditRoutes); +// Development/test routes (only in development) +if (process.env.NODE_ENV !== 'production') { + const testRoutes = require('./test.routes'); + router.use('/test', testRoutes); +} + +// Mount routes +router.use('/auth', authRoutes); +router.use('/documents', documentsRoutes); +router.use('/blog', blogRoutes); +router.use('/newsletter', newsletterRoutes); +router.use('/media', mediaRoutes); +router.use('/cases', casesRoutes); +router.use('/admin', adminRoutes); +router.use('/admin/hooks', hooksMetricsRoutes); +router.use('/admin/sync', syncHealthRoutes); +router.use('/admin/rules', rulesRoutes); +router.use('/admin/projects', projectsRoutes); +router.use('/admin', auditRoutes); router.use('/governance', governanceRoutes); +router.use('/koha', kohaRoutes); +router.use('/demo', demoRoutes); -// API root endpoint +// API root endpoint - redirect browsers to documentation router.get('/', (req, res) => { + // Check if request is from a browser (Accept: text/html) + const acceptsHtml = req.accepts('html'); + const acceptsJson = req.accepts('json'); + + // If browser request, redirect to API documentation page + if (acceptsHtml && !acceptsJson) { + return res.redirect(302, '/api-reference.html'); + } + res.json({ name: 'Tractatus AI Safety Framework API', - version: '3.5.0', + version: '1.0.0', status: 'operational', - documentation: 'https://agenticgovernance.digital', endpoints: { + auth: { + login: 'POST /api/auth/login', + me: 'GET /api/auth/me', + logout: 'POST /api/auth/logout' + }, + documents: { + list: 'GET /api/documents', + get: 'GET /api/documents/:identifier', + search: 'GET /api/documents/search?q=query', + create: 'POST /api/documents (admin)', + update: 'PUT /api/documents/:id (admin)', + delete: 'DELETE /api/documents/:id (admin)' + }, + blog: { + list: 'GET /api/blog', + get: 'GET /api/blog/:slug', + create: 'POST /api/blog (admin)', + update: 'PUT /api/blog/:id (admin)', + publish: 'POST /api/blog/:id/publish (admin)', + delete: 'DELETE /api/blog/:id (admin)', + admin_list: 'GET /api/blog/admin/posts?status=draft (admin)', + admin_get: 'GET /api/blog/admin/:id (admin)', + suggest_topics: 'POST /api/blog/suggest-topics (admin)' + }, + newsletter: { + subscribe: 'POST /api/newsletter/subscribe', + verify: 'GET /api/newsletter/verify/:token', + unsubscribe: 'POST /api/newsletter/unsubscribe', + preferences: 'PUT /api/newsletter/preferences', + stats: 'GET /api/newsletter/admin/stats (admin)', + subscriptions: 'GET /api/newsletter/admin/subscriptions (admin)', + export: 'GET /api/newsletter/admin/export (admin)', + delete: 'DELETE /api/newsletter/admin/subscriptions/:id (admin)' + }, + media: { + submit: 'POST /api/media/inquiries', + list: 'GET /api/media/inquiries (admin)', + urgent: 'GET /api/media/inquiries/urgent (admin)', + get: 'GET /api/media/inquiries/:id (admin)', + assign: 'POST /api/media/inquiries/:id/assign (admin)', + respond: 'POST /api/media/inquiries/:id/respond (admin)', + delete: 'DELETE /api/media/inquiries/:id (admin)' + }, + cases: { + submit: 'POST /api/cases/submit', + list: 'GET /api/cases/submissions (admin)', + high_relevance: 'GET /api/cases/submissions/high-relevance (admin)', + get: 'GET /api/cases/submissions/:id (admin)', + approve: 'POST /api/cases/submissions/:id/approve (admin)', + reject: 'POST /api/cases/submissions/:id/reject (admin)', + request_info: 'POST /api/cases/submissions/:id/request-info (admin)', + delete: 'DELETE /api/cases/submissions/:id (admin)' + }, + admin: { + moderation_queue: 'GET /api/admin/moderation', + moderation_item: 'GET /api/admin/moderation/:id', + review: 'POST /api/admin/moderation/:id/review', + stats: 'GET /api/admin/stats', + activity: 'GET /api/admin/activity' + }, governance: { status: 'GET /api/governance', - classify: 'POST /api/governance/classify', - validate: 'POST /api/governance/validate', - enforce: 'POST /api/governance/enforce', - pressure: 'POST /api/governance/pressure', - verify: 'POST /api/governance/verify' + classify: 'POST /api/governance/classify (admin)', + validate: 'POST /api/governance/validate (admin)', + enforce: 'POST /api/governance/enforce (admin)', + pressure: 'POST /api/governance/pressure (admin)', + verify: 'POST /api/governance/verify (admin)' }, - rules: { - list: 'GET /api/rules', - get: 'GET /api/rules/:id', - create: 'POST /api/rules', - update: 'PUT /api/rules/:id', - delete: 'DELETE /api/rules/:id', - search: 'GET /api/rules/search' - }, - projects: { - list: 'GET /api/projects', - get: 'GET /api/projects/:id', - create: 'POST /api/projects', - update: 'PUT /api/projects/:id', - delete: 'DELETE /api/projects/:id' - }, - audit: { - logs: 'GET /api/audit/logs', - stats: 'GET /api/audit/stats' + koha: { + checkout: 'POST /api/koha/checkout', + webhook: 'POST /api/koha/webhook', + transparency: 'GET /api/koha/transparency', + cancel: 'POST /api/koha/cancel', + verify: 'GET /api/koha/verify/:sessionId', + statistics: 'GET /api/koha/statistics (admin)' } }, - framework: { - name: 'Tractatus Framework', - description: 'AI governance framework enforcing architectural safety constraints at runtime', - services: [ - 'InstructionPersistenceClassifier', - 'CrossReferenceValidator', - 'BoundaryEnforcer', - 'ContextPressureMonitor', - 'MetacognitiveVerifier', - 'PluralisticDeliberationOrchestrator' - ] - } + framework: 'Tractatus-Based LLM Safety Architecture', + documentation: '/api/docs', + health: '/health' }); }); diff --git a/src/server.js b/src/server.js index 7b749c84..223e727e 100644 --- a/src/server.js +++ b/src/server.js @@ -1,6 +1,6 @@ /** - * Tractatus Framework Server - * Generic Express server template for Tractatus implementations + * Tractatus Express Server + * Main application entry point */ require('dotenv').config(); @@ -8,14 +8,21 @@ 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 } = require('./middleware/rate-limit.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(); @@ -24,35 +31,90 @@ const app = express(); app.set('trust proxy', 1); // ============================================================ -// SECURITY MIDDLEWARE +// SECURITY MIDDLEWARE (Quick Wins - inst_041-046) // ============================================================ -// Security headers +// Enhanced security headers (replaces helmet CSP with more specific policy) app.use(securityHeadersMiddleware); -// Helmet for additional security +// Keep helmet for other security features (but CSP already set above) app.use(helmet({ - contentSecurityPolicy: false // Using custom CSP in securityHeadersMiddleware + contentSecurityPolicy: false, // Disabled - using our custom CSP in securityHeadersMiddleware })); // CORS app.use(cors(config.cors)); -// Body parsers +// 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); -// Rate limiting +// 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); -// ============================================================ -// ROUTES -// ============================================================ +// Cache control middleware for static assets +app.use((req, res, next) => { + const path = req.path; -// Health check endpoint + // 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'); + } + // CSS and JS files: Longer cache (we use version parameters) + else if (path.endsWith('.css') || path.endsWith('.js')) { + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); // 1 year + } + // 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(); +}); + +// Static files +app.use(express.static('public')); + +// Health check endpoint (minimal, no sensitive data) app.get('/health', (req, res) => { res.json({ status: 'ok', @@ -60,47 +122,104 @@ app.get('/health', (req, res) => { }); }); +// 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 +// Homepage (temporary) app.get('/', (req, res) => { - res.json({ - name: 'Tractatus AI Safety Framework', - version: '3.5.0', - status: 'operational', - documentation: 'https://agenticgovernance.digital', - endpoints: { - health: 'GET /health', - api: 'GET /api' - } - }); + 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 +// ERROR HANDLING (Quick Wins) // ============================================================ // 404 handler app.use(notFound); -// Error handler +// Enhanced error handler (sanitizes responses, hides stack traces) +app.use(sanitizeErrorResponse); + +// Fallback to original error handler if needed app.use(errorHandler); -// ============================================================ -// SERVER STARTUP -// ============================================================ - +// Server startup async function start() { try { // Connect to MongoDB (native driver) await connectDb(); - logger.info('✅ MongoDB (native) connected'); // Connect Mongoose (for ODM models) await connectMongoose(); - logger.info('✅ Mongoose connected'); + + // 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'); @@ -113,12 +232,14 @@ async function start() { // Start server const server = app.listen(config.port, () => { - logger.info(`🚀 Tractatus Framework server started`); - logger.info(` Environment: ${config.env}`); - logger.info(` Port: ${config.port}`); - logger.info(` MongoDB: ${config.mongodb.db}`); - logger.info(` API: http://localhost:${config.port}/api`); - console.log(`\n🌐 Server running at http://localhost:${config.port}\n`); + 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 @@ -139,7 +260,7 @@ async function shutdown(server) { logger.info('HTTP server closed'); await closeDb(); - logger.info('MongoDB (native) connection closed'); + logger.info('Native MongoDB connection closed'); await closeMongoose(); logger.info('Mongoose connection closed');