Files
Krystie e99413150e
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
[glm-grade=B] fix: dead dashboard WS, corrupted error.log, auth-polling storm, re-auth freeze + CVE bumps
Adversarial audit 2026-08-16 (GLM-5.3 delegate, 2 rounds, 141 tool calls):

P0-1: Dashboard WebSocket (/api/v1/ws) dead on EVERY boot since DC-076.
server.js passed module exports (DependencyManager class, {AutoRestartManager}
namespace, SSLMonitor class) instead of createApp()'s live instances — first
.on() threw ERR_INVALID_ARG_TYPE, catch swallowed it. Fix: app.locals.ctx
exposed in src/app.js; server.js passes all 8 real EventEmitter instances.

P0-2: error.log corrupted since 2026-07-14. errorMiddleware called
logError(FILE, SIZE, path, err, meta) — 5 args into a 3-arg wrapper —
logging 'Error: 5242880' garbage every ~60s and DISCARDING the real error
object. Fix: correct 3-arg call + legacy-shape guard in logErrorWrapper +
~74 log.error sites swept to pass real error objects (AST-verified scope-
safe 71/71, 29/29 modules load clean).

P0-3: auth-polling storm (stranded grade=B commit never landed in prod):
401/403 behind TOTP gate hammered /api/v1/services/status + SSE reconnect
every 2-8s, with misleading direct-probe fallback marking services 'up'.
Fix landed + B-round MEDIUM follow-up: TOTP re-auth success now clears
_dcAuthLost, resumes SSE (new _sseResume clears the latch), and refreshes.

Also: eslintignore static-sites/ (33→0 errors); nodemailer 8→9.0.5 and
sharp 0.33→0.35.3 (3 high CVEs killed; jest green on new majors);
dockerode@5/uuid deferred (semver-major, Docker API surface).

Verification: 80/80 suites, 1837/1837 tests; ESLint 0 errors/743 warnings;
node --check all changed files; bundles rebuilt + SW cache bumped.

Judges: Codex quota-dead until Aug 19 (verified live) — GLM adversarial
delegate per operator directive 2026-08-07. Round 1: 98-call mechanical
verification (timed out pre-verdict). Round 2 (this grade): B, one MEDIUM
(re-auth freeze) — fixed in this commit as prescribed.
2026-08-16 04:18:07 -07:00

326 lines
12 KiB
JavaScript

/**
* DashCaddy API Server - Entry Point
* Minimal startup script - all logic moved to src/
*/
const { createApp } = require('./src/app');
const { fetchT } = require('./src/utils/http');
const platformPaths = require('./platform-paths');
// Unhandled error handlers
process.on('unhandledRejection', (reason) => {
console.error('[FATAL] Unhandled Promise Rejection:', reason);
process.exit(1);
});
process.on('uncaughtException', (error) => {
console.error('[FATAL] Uncaught Exception:', error);
setTimeout(() => process.exit(1), 1000).unref();
});
// Main startup
(async () => {
try {
// Create and configure Express app
const { app, log, config, licenseManager } = await createApp();
// Load license
await licenseManager.load();
const PORT = parseInt(process.env.PORT, 10) || 3001;
const HOST = process.env.HOST || '0.0.0.0';
const CADDYFILE_PATH = process.env.CADDYFILE_PATH || platformPaths.caddyfile;
const CADDY_ADMIN_URL = process.env.CADDY_ADMIN_URL || platformPaths.caddyAdminUrl;
const SERVICES_FILE = process.env.SERVICES_FILE || platformPaths.servicesFile;
const CONFIG_FILE = process.env.CONFIG_FILE || platformPaths.servicesFile.replace('services.json', 'config.json');
// dataDir safety guard — DC-046 follow-up to DC-039. Refuse to boot in
// production if dataDir resolved into the Docker image layer (audit-log,
// license keys, error logs etc. would silently land there and vanish on
// the next container recreate). Throws → no crash-loop, just a clear
// fatal error message before any runtime state can be written.
platformPaths.assertSafe({ mode: process.env.NODE_ENV === 'production' ? 'production' : 'development' });
// Validate startup configuration
const { validateStartupConfig } = require('./src/utilities/startup-validator');
await validateStartupConfig({
log,
CADDYFILE_PATH,
SERVICES_FILE,
CONFIG_FILE,
CADDY_ADMIN_URL,
PORT
});
// Start HTTP server
const server = app.listen(PORT, HOST, () => {
log.info('server', 'DashCaddy API server started', {
port: PORT,
host: HOST,
caddyfile: CADDYFILE_PATH,
caddyAdmin: CADDY_ADMIN_URL,
services: SERVICES_FILE,
environment: process.env.NODE_ENV || 'production'
});
// Attach WebSocket exec handler (with auth)
const attachExecWS = require('./routes/exec');
const authManager = require('./src/managers/auth-manager');
attachExecWS(server, log, authManager);
log.info('server', 'WebSocket exec handler attached (auth enforced)');
// DC-076: Attach dashboard WebSocket for real-time updates.
// createApp() returns the live manager instances — use those instead
// of re-requiring the modules (which yields singletons for some
// managers and raw classes / namespace objects for others; calling
// .on() on a class threw on every boot and silently killed the WS).
try {
const { ctx } = app.locals;
const createDashboardWS = require('./src/websocket/dashboard-ws');
createDashboardWS(server, {
resourceMonitor: ctx.resourceMonitor,
healthChecker: ctx.healthChecker,
updateManager: ctx.updateManager,
dependencyManager: ctx.dependencyManager,
autoRestartManager: ctx.autoRestartManager,
driftDetector: ctx.driftDetector,
sslMonitor: ctx.sslMonitor,
dnsPropagationChecker: ctx.dnsPropagationChecker,
log,
});
log.info('server', 'Dashboard WebSocket attached at /api/v1/ws');
} catch (err) {
log.error('server', err, null, { feature: 'dashboard-ws' });
}
// Start feature modules
const resourceMonitor = require('./src/managers/resource-monitor');
const backupManager = require('./src/utilities/backup-manager');
const healthChecker = require('./src/monitoring/health-checker');
const updateManager = require('./src/managers/update-manager');
const selfUpdater = require('./src/docker/self-updater');
const portLockManager = require('./src/managers/port-lock-manager');
// Create servicesStateManager early — needed by workflow engine init
const StateManager = require('./src/managers/state-manager');
const servicesStateManager = new StateManager(SERVICES_FILE);
// Optional modules
let dockerMaintenance, logDigest, bundledWorkflows;
try { dockerMaintenance = require('./src/docker/docker-maintenance'); } catch { /* optional */ }
try { logDigest = require('./src/security/log-digest'); } catch { /* optional */ }
try { bundledWorkflows = require('./src/recipes/bundled-workflows'); } catch { /* optional */ }
// Initialize workflow engine if bundled-workflows is available
// NOTE: createApp() already initializes the workflow engine in src/app.js
// This block is kept for backward compat with entry points that don't use createApp()
let workflowEngine = null;
if (bundledWorkflows) {
try {
const { fetchT } = require('./src/utils/http');
const { WorkflowEngine } = bundledWorkflows;
// Create a context with needed services
const workflowCtx = {
docker: { client: require('dockerode')() },
notification: new (require('./src/managers/notification-manager'))({
NOTIFICATIONS_FILE: process.env.NOTIFICATIONS_FILE || require('./platform-paths').notificationsFile,
fetchT,
log,
config
}),
backupManager,
resourceMonitor,
servicesStateManager
};
workflowEngine = new WorkflowEngine(workflowCtx);
log.info('server', 'Workflow engine initialized');
} catch (err) {
log.error('server', err, null, { note: 'Workflow engine failed to initialize' });
}
}
log.info('server', 'Starting feature modules');
// Clean up stale port locks
portLockManager.cleanupStaleLocks()
.then(() => log.info('server', 'Port lock cleanup completed'))
.catch(err => log.error('server', err, null, { note: 'Port lock cleanup failed' }));
// Resource monitoring
try {
resourceMonitor.start();
// Connect workflow engine to resource monitor for resource-alert events
if (workflowEngine) {
resourceMonitor.setWorkflowEngine(workflowEngine);
}
log.info('server', 'Resource monitoring started');
} catch (err) {
log.error('server', err, null, { note: 'Resource monitoring failed to start' });
}
// Backup manager
try {
backupManager.start();
log.info('server', 'Backup manager started');
} catch (err) {
log.error('server', err, null, { note: 'Backup manager failed to start' });
}
// Security event workers (Caddy access log, fail2ban, shared_bans)
// Each one tail-follows a log file and emits events into the unified
// security store. They survive restarts via persisted offsets.
try {
const { startAll: startSecurityWorkers } = require('./src/security/event-workers');
startSecurityWorkers({ log });
log.info('server', 'Security event workers started');
} catch (err) {
log.error('server', err, null, { note: 'Security event workers failed to start' });
}
// Connect workflow engine to update manager for pre-update events
if (workflowEngine) {
updateManager.setWorkflowEngine(workflowEngine);
}
// Health checker (with service sync)
(async () => {
try {
const { syncHealthCheckerServices } = require('./src/utilities/startup-validator');
const StateManager = require('./src/managers/state-manager');
const servicesStateManager = new StateManager(SERVICES_FILE);
await syncHealthCheckerServices({
log,
SERVICES_FILE,
servicesStateManager,
healthChecker,
buildServiceUrl: (subdomain) => config.routingMode === 'subdirectory' && config.domain
? `https://${config.domain}/${subdomain}`
: `https://${subdomain}${config.tld}`,
siteConfig: config,
APP: require('./src/utilities/constants').APP
});
healthChecker.start();
log.info('server', 'Health checker started');
} catch (err) {
log.error('server', err, null, { note: 'Health checker failed to start' });
}
})();
// Update manager
try {
updateManager.start();
log.info('server', 'Update manager started');
} catch (err) {
log.error('server', err, null, { note: 'Update manager failed to start' });
}
// Self-updater
try {
selfUpdater.start();
log.info('server', 'Self-updater started', {
interval: selfUpdater.config.checkInterval,
url: selfUpdater.config.updateUrl
});
selfUpdater.checkPostUpdateResult()
.then(result => {
if (result) {
log.info('server', 'Post-update result', result);
}
})
.catch(() => {});
} catch (err) {
log.error('server', err, null, { note: 'Self-updater failed to start' });
}
// Docker maintenance (optional)
if (dockerMaintenance) {
try {
dockerMaintenance.start();
log.info('server', 'Docker maintenance started');
dockerMaintenance.on('maintenance-complete', (result) => {
const saved = Math.round(result.spaceReclaimed.total / 1024 / 1024);
if (saved > 0 || result.warnings.length > 0) {
log.info('maintenance', 'Docker maintenance completed', {
spaceReclaimedMB: saved,
pruned: result.pruned,
warnings: result.warnings.length
});
}
if (result.warnings.length > 0) {
for (const w of result.warnings) log.warn('maintenance', w);
}
});
} catch (err) {
log.error('server', err, null, { note: 'Docker maintenance failed to start' });
}
}
// Log digest (optional)
if (logDigest) {
try {
logDigest.start(platformPaths.digestDir);
log.info('server', 'Log digest started', { digestDir: platformPaths.digestDir });
logDigest.on('digest-generated', ({ date }) => {
log.info('digest', `Daily digest generated for ${date}`);
});
} catch (err) {
log.error('server', err, null, { note: 'Log digest failed to start' });
}
}
log.info('server', 'All feature modules initialized');
});
// Graceful shutdown
const shutdown = (signal) => {
log.info('shutdown', `${signal} received, draining connections...`);
const resourceMonitor = require('./src/managers/resource-monitor');
const backupManager = require('./src/utilities/backup-manager');
const healthChecker = require('./src/monitoring/health-checker');
const updateManager = require('./src/managers/update-manager');
const selfUpdater = require('./src/docker/self-updater');
resourceMonitor.stop();
backupManager.stop();
healthChecker.stop();
updateManager.stop();
selfUpdater.stop();
try {
const dockerMaintenance = require('./src/docker/docker-maintenance');
dockerMaintenance.stop();
} catch { /* optional */ }
try {
const logDigest = require('./src/security/log-digest');
logDigest.stop();
} catch { /* optional */ }
server.close(() => {
log.info('shutdown', 'HTTP server closed');
process.exit(0);
});
// Force exit after 5s if connections don't drain
setTimeout(() => process.exit(0), 5000).unref();
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
} catch (error) {
console.error('[FATAL] Server startup failed:', error);
process.exit(1);
}
})();
// Export for testing
module.exports = require('./src/app');