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.
143 lines
4.4 KiB
JavaScript
143 lines
4.4 KiB
JavaScript
/**
|
|
* Config migration system
|
|
*
|
|
* When config.json schema changes between versions, register a migration
|
|
* function here. On load, the loader detects the stored version, runs all
|
|
* migrations from that version forward, and writes the result back.
|
|
*
|
|
* Migration format:
|
|
* migrations[<toVersion>] = (rawConfig) => { ...mutations, _version: toVersion }
|
|
*
|
|
* Each migration is responsible for transforming the previous version's
|
|
* shape into the next version's shape. They run sequentially, so v1→v2→v3
|
|
* all execute in order.
|
|
*
|
|
* For first-time users with no config file, the loader creates a fresh
|
|
* config with CURRENT_VERSION, so they start at the latest schema.
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const _platformPaths = require('../../platform-paths');
|
|
|
|
const CURRENT_VERSION = 2;
|
|
|
|
/**
|
|
* Migrations: keys are the version they PRODUCE.
|
|
* Each migration takes a raw config object and returns the next version.
|
|
*/
|
|
const migrations = {
|
|
// v0 (unversioned) → v1: add _version field, normalize dns structure
|
|
1: (raw) => {
|
|
const migrated = { ...raw };
|
|
if (!migrated._version) migrated._version = 1;
|
|
// Normalize: older configs may have dns as a string IP, convert to object
|
|
if (typeof migrated.dns === 'string') {
|
|
migrated.dns = { ip: migrated.dns, port: 5380 };
|
|
} else if (!migrated.dns) {
|
|
migrated.dns = { ip: '', port: 5380 };
|
|
}
|
|
return migrated;
|
|
},
|
|
|
|
// v1 → v2: add dns.provider field (default: 'technitium' for backwards compat)
|
|
2: (raw) => {
|
|
const migrated = { ...raw };
|
|
if (migrated.dns && !migrated.dns.provider) {
|
|
migrated.dns.provider = 'technitium';
|
|
}
|
|
migrated._version = 2;
|
|
return migrated;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Run all migrations from `fromVersion` (or detected) to CURRENT_VERSION.
|
|
* @param {object} raw - The raw config object (may or may not have _version)
|
|
* @returns {object} The migrated config
|
|
*/
|
|
function migrate(raw) {
|
|
if (!raw || typeof raw !== 'object') {
|
|
// First-time load: return minimal config at current version
|
|
return { _version: CURRENT_VERSION };
|
|
}
|
|
|
|
const fromVersion = raw._version || 0;
|
|
if (fromVersion > CURRENT_VERSION) {
|
|
// Config from a future version — bail out, don't corrupt it
|
|
// The validation step will catch any actual issues
|
|
return raw;
|
|
}
|
|
|
|
let current = { ...raw };
|
|
for (let v = fromVersion + 1; v <= CURRENT_VERSION; v++) {
|
|
if (migrations[v]) {
|
|
current = migrations[v](current);
|
|
} else {
|
|
// No migration defined for this version, just bump _version
|
|
current._version = v;
|
|
}
|
|
}
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* Load config from disk, run migrations if needed, and write back the
|
|
* migrated version. Safe to call on every startup.
|
|
* @param {string} configFile - Absolute path to config.json
|
|
* @param {object} log - Logger instance
|
|
* @returns {object} The migrated config object
|
|
*/
|
|
function loadAndMigrate(configFile, log) {
|
|
let raw = null;
|
|
let fileExisted = false;
|
|
|
|
if (fs.existsSync(configFile)) {
|
|
fileExisted = true;
|
|
try {
|
|
raw = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
} catch (e) {
|
|
if (log && log.error) {
|
|
log.error('config-migration', e, null, { note: 'Failed to parse config.json, using defaults' });
|
|
}
|
|
raw = null;
|
|
}
|
|
}
|
|
|
|
const fromVersion = raw && raw._version ? raw._version : 0;
|
|
const migrated = migrate(raw);
|
|
|
|
// Only write back to disk if:
|
|
// 1. The file already existed (we don't create configs on fresh installs —
|
|
// the loader's defaults handle that case), AND
|
|
// 2. The version actually changed (no point rewriting identical content)
|
|
if (fileExisted && fromVersion < CURRENT_VERSION) {
|
|
if (log && log.info) {
|
|
log.info('config-migration', `Migrated config v${fromVersion} → v${CURRENT_VERSION}`, {
|
|
from: fromVersion,
|
|
to: CURRENT_VERSION,
|
|
path: configFile
|
|
});
|
|
}
|
|
// Write back the migrated config
|
|
try {
|
|
// Ensure parent dir exists
|
|
const dir = path.dirname(configFile);
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(configFile, JSON.stringify(migrated, null, 2));
|
|
} catch (e) {
|
|
if (log && log.warn) {
|
|
log.warn('config-migration', 'Failed to write migrated config back to disk', { error: e.message });
|
|
}
|
|
}
|
|
}
|
|
|
|
return migrated;
|
|
}
|
|
|
|
module.exports = {
|
|
CURRENT_VERSION,
|
|
migrations,
|
|
migrate,
|
|
loadAndMigrate
|
|
};
|