- Created /source-code.html — sovereign hosting landing page explaining why we left GitHub, how to access the code, and the sovereignty model - Navbar: GitHub link → Source Code link (desktop + mobile) - Footer: GitHub link → Source Code link - Docs sidebar: GitHub section → Source Code section with sovereign repo - Implementer page: all repository links point to /source-code.html, clone instructions updated, CI/CD code example genericised - FAQ: GitHub Discussions button → Contact Us with email icon - FAQ content: all 4 locales (en/de/fr/mi) rewritten to remove GitHub Actions YAML, GitHub URLs, and GitHub-specific patterns - faq.js fallback content: same changes as locale files - agent-lightning integration page: updated to source-code.html - Project model: example URL changed from GitHub to Codeberg - All locale files updated: navbar.github → navbar.source_code, footer GitHub → source_code, FAQ button text updated in 4 languages Zero GitHub references remain in any HTML, JS, or JSON file (only github-dark.min.css theme name in highlight.js CDN reference). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
335 lines
12 KiB
JavaScript
335 lines
12 KiB
JavaScript
/**
|
|
* 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
|
|
}
|
|
// API responses: NEVER cache (security middleware sets this too, but catch-all below would override)
|
|
else if (path.startsWith('/api/')) {
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
res.setHeader('Pragma', 'no-cache');
|
|
res.setHeader('Expires', '0');
|
|
}
|
|
// Everything else (static assets without extensions): 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}`);
|
|
});
|
|
|
|
// 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(`
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Tractatus AI Safety Framework</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
max-width: 800px;
|
|
margin: 50px auto;
|
|
padding: 20px;
|
|
line-height: 1.6;
|
|
}
|
|
h1 { color: #2563eb; }
|
|
.status { color: #059669; font-weight: bold; }
|
|
code { background: #f3f4f6; padding: 2px 6px; border-radius: 3px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Tractatus AI Safety Framework</h1>
|
|
<p class="status">✓ Server Running</p>
|
|
<p>Development environment for the Tractatus-Based LLM Safety Framework website.</p>
|
|
|
|
<h2>Status</h2>
|
|
<ul>
|
|
<li>✓ MongoDB connected (port 27017)</li>
|
|
<li>✓ Express server running (port ${config.port})</li>
|
|
<li>✓ Database initialized (10 collections)</li>
|
|
<li>✓ Core models implemented</li>
|
|
<li>✓ API routes complete (auth, documents, blog, admin)</li>
|
|
<li>✓ Governance services active (6 core services)</li>
|
|
<li>⏳ Frontend (pending)</li>
|
|
</ul>
|
|
|
|
<h2>Available Endpoints</h2>
|
|
<ul>
|
|
<li><code>GET /health</code> - Health check</li>
|
|
<li><code>GET /api</code> - API documentation</li>
|
|
<li><code>POST /api/auth/login</code> - Admin login</li>
|
|
<li><code>GET /api/documents</code> - List framework documents</li>
|
|
<li><code>GET /api/blog</code> - List published blog posts</li>
|
|
<li><code>GET /api/admin/stats</code> - System statistics (auth required)</li>
|
|
</ul>
|
|
|
|
<p><em>Phase 1 Development - Not for public use</em></p>
|
|
</body>
|
|
</html>
|
|
`);
|
|
});
|
|
|
|
// ============================================================
|
|
// 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;
|