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.
This commit is contained in:
TheFlow 2025-10-23 10:57:20 +13:00
parent 792a9e55b6
commit 4c656385fe
4 changed files with 317 additions and 101 deletions

View file

@ -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

View file

@ -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
};

View file

@ -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'
});
});

View file

@ -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(`
<!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
// 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');