Fail closed on diagnostic config and history reads
Diagnostics previously reused the recovery-oriented ConfigStore readers. A missing, unreadable, or corrupt primary config could therefore be replaced by cached, backup, or default data, leaving diagnostics without a trustworthy decrypted secret set. Dedicated history failures and invalid payloads could likewise become a healthy empty result or stale config history. Add explicit diagnostic config and history reader contracts. The config path bypasses caches and recovery fallbacks, validates the primary document, decrypts its current credentials, and propagates read, parse, validation, and decryption failures. The history path accepts a valid empty array, rejects unreadable or malformed dedicated data, and uses strict legacy config history only when no dedicated file exists. Keep the normal UI recovery readers unchanged and wire diagnostics to the strict contracts. Cover primary recovery isolation, decryption failure propagation, valid empty history, corrupt and unreadable history, stale-history fallback prevention, pre-migration compatibility, and main-process wiring. Existing collector tests continue to prove successful responses, response-boundary redaction, shared history semantics, and snapshot non-mutation.
This commit is contained in:
+50
-13
@@ -224,6 +224,15 @@ class ConfigStore {
|
||||
}
|
||||
}
|
||||
|
||||
_readHistoryFileStrict() {
|
||||
const raw = fs.readFileSync(this.historyPath, 'utf-8');
|
||||
if (!raw || raw.trim().length < 2) throw new Error('Die Diagnoseverlaufsdatei ist ungültig');
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
if (parsed && Array.isArray(parsed.history)) return parsed.history;
|
||||
throw new Error('Die Diagnoseverlaufsdatei ist ungültig');
|
||||
}
|
||||
|
||||
_writeHistoryFileDurable(arr) {
|
||||
const tmp = this.historyPath + '.tmp';
|
||||
const fd = fs.openSync(tmp, 'w');
|
||||
@@ -380,7 +389,11 @@ class ConfigStore {
|
||||
return r;
|
||||
}
|
||||
|
||||
_loadImpl() {
|
||||
loadDiagnosticsConfig() {
|
||||
return this._loadImpl(true);
|
||||
}
|
||||
|
||||
_loadImpl(strict = false) {
|
||||
try {
|
||||
// In-memory cache keyed on the file's mtime+size. The processed config
|
||||
// (merged + credential-decrypted) is reparsed/re-decrypted from disk ONLY
|
||||
@@ -392,21 +405,33 @@ class ConfigStore {
|
||||
// long-running main-thread drag. load() always returns a CLONE so callers
|
||||
// can mutate the result without corrupting the cache.
|
||||
let stat = null;
|
||||
try { stat = fs.statSync(this.filePath); } catch {}
|
||||
if (!strict) {
|
||||
try { stat = fs.statSync(this.filePath); } catch {}
|
||||
}
|
||||
const statKey = stat ? `${stat.mtimeMs}:${stat.size}` : '';
|
||||
if (stat && this._cache && this._cacheKey === statKey) {
|
||||
if (!strict && stat && this._cache && this._cacheKey === statKey) {
|
||||
return this._clone(this._cache);
|
||||
}
|
||||
|
||||
let data = null;
|
||||
// Try main config
|
||||
try { data = this._readAndParse(this.filePath); } catch {}
|
||||
// Fallback to backup if main is empty/corrupt
|
||||
if (!data) {
|
||||
try { data = this._readAndParse(this.filePath + '.bak'); } catch {}
|
||||
}
|
||||
if (!data) {
|
||||
try { data = this._readAndParse(this.filePath + '.pre-history-split.bak'); } catch {}
|
||||
if (strict) {
|
||||
data = this._readAndParse(this.filePath);
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data) ||
|
||||
!data.hosters || typeof data.hosters !== 'object' || Array.isArray(data.hosters) ||
|
||||
!data.globalSettings || typeof data.globalSettings !== 'object' || Array.isArray(data.globalSettings) ||
|
||||
(data.history !== undefined && !Array.isArray(data.history))) {
|
||||
throw new Error('Die Diagnosekonfiguration ist ungültig');
|
||||
}
|
||||
} else {
|
||||
// Try main config
|
||||
try { data = this._readAndParse(this.filePath); } catch {}
|
||||
// Fallback to backup if main is empty/corrupt
|
||||
if (!data) {
|
||||
try { data = this._readAndParse(this.filePath + '.bak'); } catch {}
|
||||
}
|
||||
if (!data) {
|
||||
try { data = this._readAndParse(this.filePath + '.pre-history-split.bak'); } catch {}
|
||||
}
|
||||
}
|
||||
if (!data) {
|
||||
const fresh = JSON.parse(JSON.stringify(DEFAULTS));
|
||||
@@ -487,13 +512,13 @@ class ConfigStore {
|
||||
// Decrypt credentials stored with safeStorage so the rest of the app
|
||||
// keeps working with plaintext in memory.
|
||||
secretStore.decryptCredentials(result);
|
||||
if (stat) {
|
||||
if (!strict && stat) {
|
||||
this._cache = result;
|
||||
this._cacheKey = statKey;
|
||||
}
|
||||
return this._clone(result);
|
||||
} catch (error) {
|
||||
if (error instanceof secretStore.SecretStoreError) throw error;
|
||||
if (strict || error instanceof secretStore.SecretStoreError) throw error;
|
||||
const fresh = JSON.parse(JSON.stringify(DEFAULTS));
|
||||
fresh.globalSettings.logMode = normalizeLogMode(fresh.globalSettings);
|
||||
return fresh;
|
||||
@@ -707,6 +732,18 @@ class ConfigStore {
|
||||
return config.history || [];
|
||||
}
|
||||
|
||||
loadDiagnosticsHistory() {
|
||||
if (this._historyMigrated) return this._readHistoryFileStrict();
|
||||
try {
|
||||
return this._readHistoryFileStrict();
|
||||
} catch (error) {
|
||||
if (!error || error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
const config = this.loadDiagnosticsConfig();
|
||||
if (!Array.isArray(config.history)) throw new Error('Der Diagnoseverlauf ist ungültig');
|
||||
return config.history;
|
||||
}
|
||||
|
||||
_atomicWrite(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tmpPath = this.filePath + '.tmp';
|
||||
|
||||
Reference in New Issue
Block a user