refactor: remove project-specific code and fix broken imports (Phase 7)

CRITICAL FIX: src/routes/index.js was importing 10 non-existent route files
- Repository would CRASH ON STARTUP

REMOVED (8 files):
- src/config/currencies.config.js - Koha donation system (10 currencies, exchange rates)
- src/routes/hooks-metrics.routes.js - Required deleted auth.middleware
- src/routes/sync-health.routes.js - Required deleted auth.middleware
- src/utils/security-logger.js - Hardcoded /var/log/tractatus paths, OUR inst_046
- scripts/seed-admin.js - Required deleted User.model
- scripts/validate-deployment.js - OUR deployment validation (inst_025)
- systemd/tractatus-dev.service - OUR server at /var/www/tractatus
- systemd/tractatus-prod.service - OUR production server config

REWRITTEN (2 files):
src/routes/index.js
- Removed imports: auth, documents, blog, newsletter, media, cases, admin, koha, demo, test
- Removed imports: hooks-metrics, sync-health (just deleted)
- Keep only: rules, projects, audit, governance (framework routes)
- Removed website endpoint documentation
- Updated to framework v3.5.0

src/config/app.config.js
- Removed: JWT config (auth system deleted)
- Removed: admin.email = john.stroh.nz@pm.me (hardcoded project-specific)
- Removed: features.aiCuration/mediaTriage/caseSubmissions (website features)
- Keep only: server, mongodb, logging, security (rate limiting), CORS
- Now generic template for implementers

RESULT: Repository can now start without errors, all imports resolve

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TheFlow 2025-10-21 22:06:43 +13:00
parent 6496e0d811
commit 5ca2777815
10 changed files with 46 additions and 1092 deletions

View file

@ -1,113 +0,0 @@
#!/usr/bin/env node
/**
* Seed Admin User Script
* Creates the initial admin user for the Tractatus platform
*
* Usage: npm run seed:admin
*/
require('dotenv').config();
const readline = require('readline');
const { connect, close } = require('../src/utils/db.util');
const User = require('../src/models/User.model');
const logger = require('../src/utils/logger.util');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function question(prompt) {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
}
async function seedAdmin() {
try {
console.log('\n=== Tractatus Admin User Setup ===\n');
// Connect to database
await connect();
// Check if admin user already exists
const existingAdmin = await User.findByEmail(process.env.ADMIN_EMAIL || 'admin@tractatus.local');
if (existingAdmin) {
console.log('⚠️ Admin user already exists.');
const overwrite = await question('Do you want to delete and recreate? (yes/no): ');
if (overwrite.toLowerCase() !== 'yes') {
console.log('Cancelled. No changes made.');
await cleanup();
return;
}
await User.deleteOne({ _id: existingAdmin._id });
console.log('✅ Existing admin user deleted.');
}
// Get admin details
console.log('\nEnter admin user details:');
const name = await question('Name (default: Admin User): ') || 'Admin User';
const email = await question(`Email (default: ${process.env.ADMIN_EMAIL || 'admin@tractatus.local'}): `)
|| process.env.ADMIN_EMAIL
|| 'admin@tractatus.local';
// Password input (hidden)
console.log('\n⚠ Password will be visible. Use a development password only.');
const password = await question('Password (min 8 chars): ');
if (!password || password.length < 8) {
console.error('❌ Password must be at least 8 characters.');
await cleanup();
return;
}
// Create admin user
const admin = await User.create({
name,
email,
password, // Will be hashed by the model
role: 'admin',
active: true
});
console.log('\n✅ Admin user created successfully!');
console.log('\nCredentials:');
console.log(` Email: ${admin.email}`);
console.log(` Role: ${admin.role}`);
console.log(` ID: ${admin._id}`);
console.log('\nYou can now login at: POST /api/auth/login');
console.log('');
logger.info(`Admin user created: ${admin.email}`);
} catch (error) {
console.error('\n❌ Error creating admin user:', error.message);
logger.error('Admin seed error:', error);
process.exit(1);
} finally {
await cleanup();
}
}
async function cleanup() {
rl.close();
await close();
}
// Handle Ctrl+C
process.on('SIGINT', async () => {
console.log('\n\n👋 Cancelled by user');
await cleanup();
process.exit(0);
});
// Run if called directly
if (require.main === module) {
seedAdmin();
}
module.exports = seedAdmin;

View file

@ -1,236 +0,0 @@
#!/usr/bin/env node
/**
* Pre-Deployment Validation
*
* Validates rsync/scp/deployment commands against inst_025 rules:
* - Checks if source files have different subdirectories
* - Ensures separate commands for different directory levels
* - Prevents directory structure flattening
*
* Usage:
* node scripts/validate-deployment.js --command "rsync ..."
* node scripts/validate-deployment.js --files "file1 file2" --target "remote:path"
*
* Exit codes:
* 0 = Valid deployment
* 1 = Invalid (violates inst_025)
* 2 = Error
*
* Copyright 2025 Tractatus Project
* Licensed under Apache License 2.0
*/
const fs = require('fs');
const path = require('path');
/**
* Parse rsync command to extract source files and target
*/
function parseRsyncCommand(command) {
// Match rsync with options and files
const rsyncPattern = /rsync\s+([^"'\s]+(?:\s+[^"'\s]+)*)\s+((?:[^\s]+\s+)*)([\w@.-]+:[^\s]+|[^\s]+)$/;
const match = command.match(rsyncPattern);
if (!match) {
return null;
}
// Extract flags and files
const parts = command.split(/\s+/).filter(p => p.length > 0);
const rsyncIndex = parts.findIndex(p => p === 'rsync' || p.endsWith('/rsync'));
if (rsyncIndex === -1) {
return null;
}
const filesAndTarget = parts.slice(rsyncIndex + 1);
// Last item is target
const target = filesAndTarget[filesAndTarget.length - 1];
// Everything before target that's not a flag is a file
const files = [];
for (let i = 0; i < filesAndTarget.length - 1; i++) {
const item = filesAndTarget[i];
if (!item.startsWith('-')) {
files.push(item);
}
}
return {
files,
target,
command
};
}
/**
* Check if files have different subdirectory paths
*/
function checkDirectoryMismatch(files) {
if (files.length <= 1) {
return { hasMismatch: false, directories: [] };
}
const directories = files.map(f => {
const dir = path.dirname(f);
return dir === '.' ? '' : dir;
});
const uniqueDirs = [...new Set(directories)];
return {
hasMismatch: uniqueDirs.length > 1,
directories: uniqueDirs,
filesByDir: Object.fromEntries(
uniqueDirs.map(dir => [
dir,
files.filter(f => path.dirname(f) === (dir || '.'))
])
)
};
}
/**
* Validate deployment command
*/
function validateDeployment(command) {
const parsed = parseRsyncCommand(command);
if (!parsed) {
return {
valid: false,
error: 'Could not parse rsync command',
suggestion: null
};
}
const { files, target } = parsed;
if (files.length === 0) {
return {
valid: false,
error: 'No source files specified',
suggestion: null
};
}
// Check for directory mismatch
const dirCheck = checkDirectoryMismatch(files);
if (!dirCheck.hasMismatch) {
return {
valid: true,
message: 'Deployment command is valid - all files in same directory',
files,
target
};
}
// Violation detected
return {
valid: false,
error: `inst_025 violation: Files from different subdirectories in single rsync command`,
details: {
file_count: files.length,
unique_directories: dirCheck.directories.length,
directories: dirCheck.directories,
filesByDir: dirCheck.filesByDir
},
suggestion: generateSeparateCommands(dirCheck.filesByDir, target, command)
};
}
/**
* Generate separate rsync commands for each directory
*/
function generateSeparateCommands(filesByDir, target, originalCommand) {
const commands = [];
// Extract rsync flags from original command
const flagMatch = originalCommand.match(/rsync\s+([^/\s][^\s]*)/);
const flags = flagMatch ? flagMatch[1] : '-avz --progress';
Object.entries(filesByDir).forEach(([dir, files]) => {
const targetWithDir = dir ? `${target}/${dir}/` : target;
files.forEach(file => {
const cmd = `rsync ${flags} ${file} ${targetWithDir}`;
commands.push(cmd);
});
});
return commands;
}
/**
* Display validation results
*/
function displayResults(result) {
if (result.valid) {
console.log('\x1b[32m✅ Deployment command is VALID\x1b[0m');
console.log(` Files: ${result.files.length}`);
console.log(` Target: ${result.target}`);
return 0;
}
console.log('\x1b[31m❌ Deployment command VIOLATES inst_025\x1b[0m');
console.log(`\n Error: ${result.error}\n`);
if (result.details) {
console.log(' Details:');
console.log(` File count: ${result.details.file_count}`);
console.log(` Unique directories: ${result.details.unique_directories}`);
console.log(' Directories:');
result.details.directories.forEach(dir => {
const dirDisplay = dir || '(root)';
const fileCount = result.details.filesByDir[dir].length;
console.log(`${dirDisplay} (${fileCount} files)`);
result.details.filesByDir[dir].forEach(file => {
console.log(` - ${path.basename(file)}`);
});
});
}
if (result.suggestion) {
console.log('\n \x1b[33mSuggested fix (separate commands per directory):\x1b[0m\n');
result.suggestion.forEach((cmd, i) => {
console.log(` ${i + 1}. ${cmd}`);
});
console.log('');
}
return 1;
}
/**
* Main
*/
function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
console.log('Pre-Deployment Validation');
console.log('\nUsage:');
console.log(' node scripts/validate-deployment.js --command "rsync ..."');
console.log('\nExample:');
console.log(' node scripts/validate-deployment.js --command "rsync -avz file1 file2/sub/file remote:path"');
process.exit(0);
}
const commandIndex = args.indexOf('--command');
if (commandIndex === -1 || !args[commandIndex + 1]) {
console.error('Error: --command flag required');
process.exit(2);
}
const command = args[commandIndex + 1];
const result = validateDeployment(command);
const exitCode = displayResults(result);
process.exit(exitCode);
}
main();

View file

@ -1,12 +1,13 @@
/** /**
* Application Configuration * Application Configuration
* Generic configuration template for Tractatus Framework implementations
*/ */
module.exports = { module.exports = {
// Server // Server
port: process.env.PORT || 9000, port: process.env.PORT || 9000,
env: process.env.NODE_ENV || 'development', env: process.env.NODE_ENV || 'development',
appName: process.env.APP_NAME || 'Tractatus', appName: process.env.APP_NAME || 'Tractatus Framework',
// MongoDB // MongoDB
mongodb: { mongodb: {
@ -14,30 +15,12 @@ module.exports = {
db: process.env.MONGODB_DB || 'tractatus_dev' 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
logging: { logging: {
level: process.env.LOG_LEVEL || 'info', level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || 'logs/app.log' 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
security: { security: {
rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 min rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 min

View file

@ -1,277 +0,0 @@
/**
* Currency Configuration
* Multi-currency support for Koha donation system
*
* Exchange rates based on NZD (New Zealand Dollar) as base currency
* Update rates periodically or use live API
*/
// Base prices in NZD (in cents)
const BASE_PRICES_NZD = {
tier_5: 500, // $5 NZD
tier_15: 1500, // $15 NZD
tier_50: 5000 // $50 NZD
};
// Exchange rates: 1 NZD = X currency
// Last updated: 2025-10-08
// Source: Manual calculation based on typical rates
const EXCHANGE_RATES = {
NZD: 1.0, // New Zealand Dollar (base)
USD: 0.60, // US Dollar
EUR: 0.55, // Euro
GBP: 0.47, // British Pound
AUD: 0.93, // Australian Dollar
CAD: 0.82, // Canadian Dollar
JPY: 94.0, // Japanese Yen
CHF: 0.53, // Swiss Franc
SGD: 0.81, // Singapore Dollar
HKD: 4.68 // Hong Kong Dollar
};
// Currency metadata (symbols, formatting, names)
const CURRENCY_CONFIG = {
NZD: {
symbol: '$',
code: 'NZD',
name: 'NZ Dollar',
decimals: 2,
locale: 'en-NZ',
flag: '🇳🇿'
},
USD: {
symbol: '$',
code: 'USD',
name: 'US Dollar',
decimals: 2,
locale: 'en-US',
flag: '🇺🇸'
},
EUR: {
symbol: '€',
code: 'EUR',
name: 'Euro',
decimals: 2,
locale: 'de-DE',
flag: '🇪🇺'
},
GBP: {
symbol: '£',
code: 'GBP',
name: 'British Pound',
decimals: 2,
locale: 'en-GB',
flag: '🇬🇧'
},
AUD: {
symbol: '$',
code: 'AUD',
name: 'Australian Dollar',
decimals: 2,
locale: 'en-AU',
flag: '🇦🇺'
},
CAD: {
symbol: '$',
code: 'CAD',
name: 'Canadian Dollar',
decimals: 2,
locale: 'en-CA',
flag: '🇨🇦'
},
JPY: {
symbol: '¥',
code: 'JPY',
name: 'Japanese Yen',
decimals: 0, // JPY has no decimal places
locale: 'ja-JP',
flag: '🇯🇵'
},
CHF: {
symbol: 'CHF',
code: 'CHF',
name: 'Swiss Franc',
decimals: 2,
locale: 'de-CH',
flag: '🇨🇭'
},
SGD: {
symbol: '$',
code: 'SGD',
name: 'Singapore Dollar',
decimals: 2,
locale: 'en-SG',
flag: '🇸🇬'
},
HKD: {
symbol: '$',
code: 'HKD',
name: 'Hong Kong Dollar',
decimals: 2,
locale: 'zh-HK',
flag: '🇭🇰'
}
};
// Supported currencies list (in display order)
const SUPPORTED_CURRENCIES = [
'NZD', // Default
'USD',
'EUR',
'GBP',
'AUD',
'CAD',
'JPY',
'CHF',
'SGD',
'HKD'
];
/**
* Convert NZD amount to target currency
* @param {number} amountNZD - Amount in NZD cents
* @param {string} targetCurrency - Target currency code
* @returns {number} - Amount in target currency cents
*/
function convertFromNZD(amountNZD, targetCurrency) {
const currency = targetCurrency.toUpperCase();
if (!EXCHANGE_RATES[currency]) {
throw new Error(`Unsupported currency: ${targetCurrency}`);
}
const rate = EXCHANGE_RATES[currency];
const converted = Math.round(amountNZD * rate);
return converted;
}
/**
* Convert any currency amount to NZD
* @param {number} amount - Amount in source currency cents
* @param {string} sourceCurrency - Source currency code
* @returns {number} - Amount in NZD cents
*/
function convertToNZD(amount, sourceCurrency) {
const currency = sourceCurrency.toUpperCase();
if (!EXCHANGE_RATES[currency]) {
throw new Error(`Unsupported currency: ${sourceCurrency}`);
}
const rate = EXCHANGE_RATES[currency];
const nzdAmount = Math.round(amount / rate);
return nzdAmount;
}
/**
* Get tier prices for a specific currency
* @param {string} currency - Currency code
* @returns {object} - Tier prices in target currency (cents)
*/
function getTierPrices(currency) {
const tier5 = convertFromNZD(BASE_PRICES_NZD.tier_5, currency);
const tier15 = convertFromNZD(BASE_PRICES_NZD.tier_15, currency);
const tier50 = convertFromNZD(BASE_PRICES_NZD.tier_50, currency);
return {
tier_5: tier5,
tier_15: tier15,
tier_50: tier50
};
}
/**
* Format currency amount for display
* @param {number} amountCents - Amount in cents
* @param {string} currency - Currency code
* @returns {string} - Formatted currency string (e.g., "$15.00", "¥1,400")
*/
function formatCurrency(amountCents, currency) {
const config = CURRENCY_CONFIG[currency.toUpperCase()];
if (!config) {
throw new Error(`Unsupported currency: ${currency}`);
}
const amount = amountCents / 100; // Convert cents to dollars
return new Intl.NumberFormat(config.locale, {
style: 'currency',
currency: currency.toUpperCase(),
minimumFractionDigits: config.decimals,
maximumFractionDigits: config.decimals
}).format(amount);
}
/**
* Get currency display name with flag
* @param {string} currency - Currency code
* @returns {string} - Display name (e.g., "🇺🇸 USD - US Dollar")
*/
function getCurrencyDisplayName(currency) {
const config = CURRENCY_CONFIG[currency.toUpperCase()];
if (!config) {
return currency.toUpperCase();
}
return `${config.flag} ${config.code} - ${config.name}`;
}
/**
* Validate currency code
* @param {string} currency - Currency code
* @returns {boolean} - True if supported
*/
function isSupportedCurrency(currency) {
return SUPPORTED_CURRENCIES.includes(currency.toUpperCase());
}
/**
* Get exchange rate for a currency
* @param {string} currency - Currency code
* @returns {number} - Exchange rate (1 NZD = X currency)
*/
function getExchangeRate(currency) {
return EXCHANGE_RATES[currency.toUpperCase()] || null;
}
/**
* Detect currency from user location
* This is a simplified version - in production, use IP geolocation API
* @param {string} countryCode - ISO country code (e.g., 'US', 'GB')
* @returns {string} - Suggested currency code
*/
function getCurrencyForCountry(countryCode) {
const countryToCurrency = {
'NZ': 'NZD',
'US': 'USD',
'DE': 'EUR', 'FR': 'EUR', 'IT': 'EUR', 'ES': 'EUR', 'NL': 'EUR',
'GB': 'GBP',
'AU': 'AUD',
'CA': 'CAD',
'JP': 'JPY',
'CH': 'CHF',
'SG': 'SGD',
'HK': 'HKD'
};
return countryToCurrency[countryCode.toUpperCase()] || 'NZD'; // Default to NZD
}
module.exports = {
BASE_PRICES_NZD,
EXCHANGE_RATES,
CURRENCY_CONFIG,
SUPPORTED_CURRENCIES,
convertFromNZD,
convertToNZD,
getTierPrices,
formatCurrency,
getCurrencyDisplayName,
isSupportedCurrency,
getExchangeRate,
getCurrencyForCountry
};

View file

@ -1,49 +0,0 @@
/**
* Hooks Metrics API Routes
* Serves framework enforcement metrics to admin dashboard
*/
const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const { authenticateToken, requireAdmin } = require('../middleware/auth.middleware');
const METRICS_PATH = path.join(__dirname, '../../.claude/metrics/hooks-metrics.json');
/**
* GET /api/admin/hooks/metrics
* Get current hooks metrics
*/
router.get('/metrics', authenticateToken, requireAdmin, async (req, res) => {
try {
// Check if metrics file exists
if (!fs.existsSync(METRICS_PATH)) {
return res.json({
success: true,
metrics: {
hook_executions: [],
blocks: [],
session_stats: {}
}
});
}
// Read metrics
const metricsData = fs.readFileSync(METRICS_PATH, 'utf8');
const metrics = JSON.parse(metricsData);
res.json({
success: true,
metrics: metrics
});
} catch (error) {
console.error('Error fetching hooks metrics:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch hooks metrics'
});
}
});
module.exports = router;

View file

@ -1,147 +1,71 @@
/** /**
* Routes Index * Routes Index
* Central routing configuration * Central routing configuration for Tractatus Framework API
*/ */
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
// Import route modules // Import framework 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 rulesRoutes = require('./rules.routes');
const projectsRoutes = require('./projects.routes'); const projectsRoutes = require('./projects.routes');
const auditRoutes = require('./audit.routes'); const auditRoutes = require('./audit.routes');
const governanceRoutes = require('./governance.routes'); const governanceRoutes = require('./governance.routes');
const kohaRoutes = require('./koha.routes');
const demoRoutes = require('./demo.routes');
// Development/test routes (only in development) // Mount framework routes
if (process.env.NODE_ENV !== 'production') { router.use('/rules', rulesRoutes);
const testRoutes = require('./test.routes'); router.use('/projects', projectsRoutes);
router.use('/test', testRoutes); router.use('/audit', auditRoutes);
}
// 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('/governance', governanceRoutes);
router.use('/koha', kohaRoutes);
router.use('/demo', demoRoutes);
// API root endpoint - redirect browsers to documentation // API root endpoint
router.get('/', (req, res) => { 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({ res.json({
name: 'Tractatus AI Safety Framework API', name: 'Tractatus AI Safety Framework API',
version: '1.0.0', version: '3.5.0',
status: 'operational', status: 'operational',
documentation: 'https://agenticgovernance.digital',
endpoints: { 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: { governance: {
status: 'GET /api/governance', status: 'GET /api/governance',
classify: 'POST /api/governance/classify (admin)', classify: 'POST /api/governance/classify',
validate: 'POST /api/governance/validate (admin)', validate: 'POST /api/governance/validate',
enforce: 'POST /api/governance/enforce (admin)', enforce: 'POST /api/governance/enforce',
pressure: 'POST /api/governance/pressure (admin)', pressure: 'POST /api/governance/pressure',
verify: 'POST /api/governance/verify (admin)' verify: 'POST /api/governance/verify'
}, },
koha: { rules: {
checkout: 'POST /api/koha/checkout', list: 'GET /api/rules',
webhook: 'POST /api/koha/webhook', get: 'GET /api/rules/:id',
transparency: 'GET /api/koha/transparency', create: 'POST /api/rules',
cancel: 'POST /api/koha/cancel', update: 'PUT /api/rules/:id',
verify: 'GET /api/koha/verify/:sessionId', delete: 'DELETE /api/rules/:id',
statistics: 'GET /api/koha/statistics (admin)' 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'
} }
}, },
framework: 'Tractatus-Based LLM Safety Architecture', framework: {
documentation: '/api/docs', name: 'Tractatus Framework',
health: '/health' description: 'AI governance framework enforcing architectural safety constraints at runtime',
services: [
'InstructionPersistenceClassifier',
'CrossReferenceValidator',
'BoundaryEnforcer',
'ContextPressureMonitor',
'MetacognitiveVerifier',
'PluralisticDeliberationOrchestrator'
]
}
}); });
}); });

View file

@ -1,124 +0,0 @@
/**
* Sync Health Check Routes
* Monitors synchronization between file-based instructions and MongoDB
*/
const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const { authenticateToken, requireAdmin } = require('../middleware/auth.middleware');
const GovernanceRule = require('../models/GovernanceRule.model');
const INSTRUCTION_FILE = path.join(__dirname, '../../.claude/instruction-history.json');
/**
* GET /api/admin/sync/health
* Check synchronization health between file and database
*/
router.get('/health', authenticateToken, requireAdmin, async (req, res) => {
try {
let fileInstructions = [];
let fileError = null;
if (fs.existsSync(INSTRUCTION_FILE)) {
try {
const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8'));
fileInstructions = (fileData.instructions || []).filter(i => i.active !== false);
} catch (err) {
fileError = err.message;
}
} else {
fileError = 'File not found';
}
const dbRules = await GovernanceRule.find({ active: true }).lean();
const fileCount = fileInstructions.length;
const dbCount = dbRules.length;
const difference = Math.abs(fileCount - dbCount);
const diffPercent = fileCount > 0 ? ((difference / fileCount) * 100).toFixed(1) : 0;
let status = 'healthy';
let message = 'File and database are synchronized';
let severity = 'success';
if (fileError) {
status = 'error';
message = 'Cannot read instruction file: ' + fileError;
severity = 'error';
} else if (difference === 0) {
status = 'healthy';
message = 'Perfectly synchronized';
severity = 'success';
} else if (difference <= 2) {
status = 'warning';
message = 'Minor desync: ' + difference + ' instruction' + (difference !== 1 ? 's' : '') + ' differ';
severity = 'warning';
} else if (difference <= 5) {
status = 'warning';
message = 'Moderate desync: ' + difference + ' instructions differ (' + diffPercent + '%)';
severity = 'warning';
} else {
status = 'critical';
message = 'Critical desync: ' + difference + ' instructions differ (' + diffPercent + '%)';
severity = 'error';
}
const fileIds = new Set(fileInstructions.map(i => i.id));
const dbIds = new Set(dbRules.map(r => r.id));
const missingInDb = fileInstructions
.filter(i => !dbIds.has(i.id))
.map(i => ({ id: i.id, text: i.text.substring(0, 60) + '...' }));
const orphanedInDb = dbRules
.filter(r => !fileIds.has(r.id))
.map(r => ({ id: r.id, text: r.text.substring(0, 60) + '...' }));
res.json({
success: true,
health: {
status,
message,
severity,
timestamp: new Date().toISOString(),
counts: { file: fileCount, database: dbCount, difference, differencePercent: parseFloat(diffPercent) },
details: { missingInDatabase: missingInDb, orphanedInDatabase: orphanedInDb },
recommendations: difference > 0 ? [
'Run: node scripts/sync-instructions-to-db.js --force',
'Or restart the server (auto-sync on startup)',
'Or wait for next session initialization'
] : []
}
});
} catch (error) {
console.error('Sync health check error:', error);
res.status(500).json({ success: false, error: 'Failed to check sync health', message: error.message });
}
});
/**
* POST /api/admin/sync/trigger
* Manually trigger synchronization
*/
router.post('/trigger', authenticateToken, requireAdmin, async (req, res) => {
try {
const { syncInstructions } = require('../../scripts/sync-instructions-to-db.js');
const result = await syncInstructions({ silent: true });
if (result.success) {
res.json({
success: true,
message: 'Synchronization completed successfully',
result: { added: result.added, updated: result.updated, deactivated: result.deactivated, finalCount: result.finalCount }
});
} else {
res.status(500).json({ success: false, error: 'Synchronization failed', message: result.error || 'Unknown error' });
}
} catch (error) {
console.error('Manual sync trigger error:', error);
res.status(500).json({ success: false, error: 'Failed to trigger synchronization', message: error.message });
}
});
module.exports = router;

View file

@ -1,72 +0,0 @@
/**
* Security Event Logger (inst_046 - Quick Win Version)
* Centralized logging for all security events
*
* QUICK WIN: Simple file-based logging with JSON format
* Full version in Phase 5 will add ProtonMail/Signal alerts
*/
const fs = require('fs').promises;
const path = require('path');
const SECURITY_LOG_PATH = process.env.SECURITY_LOG_PATH ||
(process.env.HOME ? `${process.env.HOME}/var/log/tractatus/security-audit.log` : '/var/log/tractatus/security-audit.log');
/**
* Log a security event to audit trail
*
* @param {Object} event - Security event details
* @param {string} event.type - Event type (e.g., 'rate_limit_violation')
* @param {string} event.sourceIp - Source IP address
* @param {string} event.userId - User ID (if authenticated)
* @param {string} event.endpoint - Request endpoint
* @param {string} event.userAgent - User agent string
* @param {Object} event.details - Additional event details
* @param {string} event.action - Action taken (e.g., 'blocked', 'logged')
* @param {string} event.severity - Severity level ('low', 'medium', 'high', 'critical')
*/
async function logSecurityEvent(event) {
const logEntry = {
timestamp: new Date().toISOString(),
event_type: event.type || 'unknown',
source_ip: event.sourceIp || 'unknown',
user_id: event.userId || 'anonymous',
endpoint: event.endpoint || 'unknown',
user_agent: event.userAgent || 'unknown',
violation_details: event.details || {},
action_taken: event.action || 'logged',
severity: event.severity || 'medium'
};
const logLine = JSON.stringify(logEntry) + '\n';
try {
// Ensure log directory exists
const logDir = path.dirname(SECURITY_LOG_PATH);
await fs.mkdir(logDir, { recursive: true, mode: 0o750 });
// Append to log file
await fs.appendFile(SECURITY_LOG_PATH, logLine, { encoding: 'utf-8' });
} catch (error) {
// Fallback to console if file logging fails
console.error('[SECURITY LOGGER ERROR]', error.message);
console.error('[SECURITY EVENT]', logEntry);
}
}
/**
* Helper: Extract client IP from request (handles proxies)
*/
function getClientIp(req) {
return (
req.ip ||
req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
req.connection.remoteAddress ||
'unknown'
);
}
module.exports = {
logSecurityEvent,
getClientIp
};

View file

@ -1,41 +0,0 @@
[Unit]
Description=Tractatus AI Safety Framework (Development)
Documentation=https://tractatus.sydigital.co.nz
After=network.target mongod.service
Wants=mongod.service
[Service]
Type=simple
User=theflow
Group=theflow
WorkingDirectory=/home/theflow/projects/tractatus
# Environment
Environment=NODE_ENV=development
Environment=PORT=9000
EnvironmentFile=/home/theflow/projects/tractatus/.env
# Execution
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=10
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/theflow/projects/tractatus/logs
ReadWritePaths=/home/theflow/projects/tractatus/uploads
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=tractatus-dev
# Resource limits
LimitNOFILE=65536
MemoryLimit=1G
[Install]
WantedBy=multi-user.target

View file

@ -1,41 +0,0 @@
[Unit]
Description=Tractatus AI Safety Framework (Production)
Documentation=https://tractatus.sydigital.co.nz
After=network.target mongod.service
Wants=mongod.service
[Service]
Type=simple
User=ubuntu
Group=ubuntu
WorkingDirectory=/var/www/tractatus
# Environment
Environment=NODE_ENV=production
Environment=PORT=9000
EnvironmentFile=/var/www/tractatus/.env
# Execution
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=10
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/tractatus/logs
ReadWritePaths=/var/www/tractatus/uploads
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=tractatus
# Resource limits
LimitNOFILE=65536
MemoryLimit=2G
[Install]
WantedBy=multi-user.target