fix: complete managed backup key remediation
Recover from structurally valid but undecryptable primary keyrings by selecting a fully cryptographically validated backup or recovery temp and reporting KEYRING_RECOVERED. Keep committed primary writes successful when best-effort post-publication cleanup fails, preventing ambiguous retries after durable local publication. Intercept clipboard writes inside the hidden Electron Main-process integration probe, remove the duplicate key-created toast, and make authoritative refresh coverage differ from optimistic state.
This commit is contained in:
@@ -216,24 +216,34 @@ function createOnlineBackupKeyring({
|
|||||||
async function readState() {
|
async function readState() {
|
||||||
const primary = await readCandidate(filePath);
|
const primary = await readCandidate(filePath);
|
||||||
if (primary.status === 'valid') {
|
if (primary.status === 'valid') {
|
||||||
|
const primaryState = inspectSource({ ...primary, recovered: false });
|
||||||
let primaryModified = 0;
|
let primaryModified = 0;
|
||||||
try {
|
try {
|
||||||
primaryModified = (await fsImpl.stat(filePath)).mtimeMs;
|
primaryModified = (await fsImpl.stat(filePath)).mtimeMs;
|
||||||
} catch {}
|
} catch {}
|
||||||
const candidates = await recoveryCandidates();
|
const candidates = await recoveryCandidates();
|
||||||
|
if (!firstBlockingIssue(primaryState)) {
|
||||||
|
for (const candidatePath of candidates) {
|
||||||
|
if (!isTemporaryFileName(path.basename(candidatePath))) continue;
|
||||||
|
let candidateModified = 0;
|
||||||
|
try {
|
||||||
|
candidateModified = (await fsImpl.stat(candidatePath)).mtimeMs;
|
||||||
|
} catch {}
|
||||||
|
if (candidateModified <= primaryModified) continue;
|
||||||
|
const candidate = await readCandidate(candidatePath);
|
||||||
|
if (candidate.status !== 'valid') continue;
|
||||||
|
const state = inspectSource({ ...candidate, recovered: true });
|
||||||
|
if (!firstBlockingIssue(state)) return state;
|
||||||
|
}
|
||||||
|
return primaryState;
|
||||||
|
}
|
||||||
for (const candidatePath of candidates) {
|
for (const candidatePath of candidates) {
|
||||||
if (!isTemporaryFileName(path.basename(candidatePath))) continue;
|
|
||||||
let candidateModified = 0;
|
|
||||||
try {
|
|
||||||
candidateModified = (await fsImpl.stat(candidatePath)).mtimeMs;
|
|
||||||
} catch {}
|
|
||||||
if (candidateModified <= primaryModified) continue;
|
|
||||||
const candidate = await readCandidate(candidatePath);
|
const candidate = await readCandidate(candidatePath);
|
||||||
if (candidate.status !== 'valid') continue;
|
if (candidate.status !== 'valid') continue;
|
||||||
const state = inspectSource({ ...candidate, recovered: true });
|
const state = inspectSource({ ...candidate, recovered: true });
|
||||||
if (!firstBlockingIssue(state)) return state;
|
if (!firstBlockingIssue(state)) return state;
|
||||||
}
|
}
|
||||||
return inspectSource({ ...primary, recovered: false });
|
return primaryState;
|
||||||
}
|
}
|
||||||
const candidates = await recoveryCandidates();
|
const candidates = await recoveryCandidates();
|
||||||
let fallback = null;
|
let fallback = null;
|
||||||
@@ -363,7 +373,9 @@ function createOnlineBackupKeyring({
|
|||||||
} catch {
|
} catch {
|
||||||
if (!primaryPublished) throw issueError(KEYRING_ERROR_CODES.structure);
|
if (!primaryPublished) throw issueError(KEYRING_ERROR_CODES.structure);
|
||||||
}
|
}
|
||||||
await cleanupTemporaryFiles(recoveryPath);
|
try {
|
||||||
|
await cleanupTemporaryFiles(recoveryPath);
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function list() {
|
async function list() {
|
||||||
|
|||||||
@@ -2977,7 +2977,6 @@ async function doOnlineBackupCreate() {
|
|||||||
if (!upsertManagedOnlineBackup(result.entry)) setManagedOnlineBackupRefreshIssue('Online-Sicherungen konnten nicht geladen werden', 'error');
|
if (!upsertManagedOnlineBackup(result.entry)) setManagedOnlineBackupRefreshIssue('Online-Sicherungen konnten nicht geladen werden', 'error');
|
||||||
await loadManagedOnlineBackups({ mutationGeneration: authority.mutationGeneration });
|
await loadManagedOnlineBackups({ mutationGeneration: authority.mutationGeneration });
|
||||||
setOnlineBackupStatus('Neuer Schlüssel erstellt.', 'success', authority.statusContext);
|
setOnlineBackupStatus('Neuer Schlüssel erstellt.', 'success', authority.statusContext);
|
||||||
showCopyToast('Online-Schlüssel erstellt');
|
|
||||||
} catch {
|
} catch {
|
||||||
setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error', authority.statusContext);
|
setOnlineBackupStatus('Online-Sicherung konnte nicht erstellt werden', 'error', authority.statusContext);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -462,7 +462,6 @@
|
|||||||
['Log-Pfad automatisch auf funktionierenden Ordner gesetzt', 'Log path automatically changed to a writable folder'],
|
['Log-Pfad automatisch auf funktionierenden Ordner gesetzt', 'Log path automatically changed to a writable folder'],
|
||||||
['Neuer Schlüssel erstellt.', 'New key created.'],
|
['Neuer Schlüssel erstellt.', 'New key created.'],
|
||||||
['Nicht alle Einstellungen konnten gespeichert werden', 'Not all settings could be saved'],
|
['Nicht alle Einstellungen konnten gespeichert werden', 'Not all settings could be saved'],
|
||||||
['Online-Schlüssel erstellt', 'Online key created'],
|
|
||||||
['Online-Schlüssel kopiert', 'Online key copied'],
|
['Online-Schlüssel kopiert', 'Online key copied'],
|
||||||
['Online-Sicherung konnte nicht erstellt werden', 'Online backup could not be created'],
|
['Online-Sicherung konnte nicht erstellt werden', 'Online backup could not be created'],
|
||||||
['Online-Sicherungen konnten nicht geladen werden', 'Online backups could not be loaded'],
|
['Online-Sicherungen konnten nicht geladen werden', 'Online backups could not be loaded'],
|
||||||
|
|||||||
@@ -339,6 +339,30 @@ describe('encrypted online backup keyring', () => {
|
|||||||
assert.deepEqual(snapshot.issues, ['KEYRING_RECOVERED']);
|
assert.deepEqual(snapshot.issues, ['KEYRING_RECOVERED']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recovers a cryptographically valid backup when the primary only passes structural validation', async () => {
|
||||||
|
const first = fixture();
|
||||||
|
const key = validKey();
|
||||||
|
await first.keyring.commit(first.keyring.prepare(key, timestamp));
|
||||||
|
writeKeyring(first.filePath, [{
|
||||||
|
id: parseOnlineBackupKey(key).id,
|
||||||
|
encryptedKey: 'enc:v1:YWJjZA',
|
||||||
|
createdAt: timestamp
|
||||||
|
}]);
|
||||||
|
const { createOnlineBackupKeyring } = require('../lib/online-backup-keyring');
|
||||||
|
const recovered = createOnlineBackupKeyring({
|
||||||
|
filePath: first.filePath,
|
||||||
|
encryptField: encrypt,
|
||||||
|
decryptField: decrypt,
|
||||||
|
isEncrypted: isCanonicalEnvelope
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = await recovered.list();
|
||||||
|
|
||||||
|
assert.deepEqual(snapshot.entries.map(entry => entry.id), [parseOnlineBackupKey(key).id]);
|
||||||
|
assert.deepEqual(snapshot.issues, ['KEYRING_RECOVERED']);
|
||||||
|
assert.equal(await recovered.getKey(parseOnlineBackupKey(key).id), key);
|
||||||
|
});
|
||||||
|
|
||||||
it('skips a newer cryptographically invalid recovery temp in favor of the validated backup', async () => {
|
it('skips a newer cryptographically invalid recovery temp in favor of the validated backup', async () => {
|
||||||
const first = fixture();
|
const first = fixture();
|
||||||
const key = validKey();
|
const key = validKey();
|
||||||
@@ -421,6 +445,37 @@ describe('encrypted online backup keyring', () => {
|
|||||||
assert.equal(await first.keyring.getKey(parseOnlineBackupKey(firstKey).id), firstKey);
|
assert.equal(await first.keyring.getKey(parseOnlineBackupKey(firstKey).id), firstKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('commits successfully once the primary is published even when later cleanup fails', async () => {
|
||||||
|
const first = fixture();
|
||||||
|
const firstKey = validKey();
|
||||||
|
const secondKey = validKey();
|
||||||
|
await first.keyring.commit(first.keyring.prepare(firstKey, timestamp));
|
||||||
|
let readdirCalls = 0;
|
||||||
|
const fsImpl = {
|
||||||
|
...fs.promises,
|
||||||
|
readdir: async (...args) => {
|
||||||
|
readdirCalls++;
|
||||||
|
if (readdirCalls === 2) throw Object.assign(new Error('cleanup failed'), { code: 'EIO' });
|
||||||
|
return fs.promises.readdir(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const { createOnlineBackupKeyring } = require('../lib/online-backup-keyring');
|
||||||
|
const keyring = createOnlineBackupKeyring({
|
||||||
|
filePath: first.filePath,
|
||||||
|
encryptField: encrypt,
|
||||||
|
decryptField: decrypt,
|
||||||
|
isEncrypted: isCanonicalEnvelope,
|
||||||
|
fsImpl
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(await keyring.commit(keyring.prepare(secondKey, '2026-08-22T11:00:00.000Z')), true);
|
||||||
|
assert.equal(await keyring.getKey(parseOnlineBackupKey(secondKey).id), secondKey);
|
||||||
|
assert.deepEqual((await keyring.list()).entries.map(entry => entry.id), [
|
||||||
|
parseOnlineBackupKey(secondKey).id,
|
||||||
|
parseOnlineBackupKey(firstKey).id
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('types invalid documents and never includes plaintext or ciphertext in errors', async () => {
|
it('types invalid documents and never includes plaintext or ciphertext in errors', async () => {
|
||||||
const { filePath, backupPath, keyring } = fixture();
|
const { filePath, backupPath, keyring } = fixture();
|
||||||
const key = validKey();
|
const key = validKey();
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ const managedOnlineBackupIds = {
|
|||||||
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||||
b: 'AQEBAQEBAQEBAQEBAQEBAQ',
|
b: 'AQEBAQEBAQEBAQEBAQEBAQ',
|
||||||
c: 'AgICAgICAgICAgICAgICAg',
|
c: 'AgICAgICAgICAgICAgICAg',
|
||||||
d: 'AwMDAwMDAwMDAwMDAwMDAw'
|
d: 'AwMDAwMDAwMDAwMDAwMDAw',
|
||||||
|
e: 'BAQEBAQEBAQEBAQEBAQEBA'
|
||||||
};
|
};
|
||||||
const managedOnlineBackupListResponses = [
|
const managedOnlineBackupListResponses = [
|
||||||
{ ok: true, warningCode: 'KEYRING_DECRYPT_FAILED', warning: 'Gespeicherter Online-Sicherungsschlüssel konnte nicht entschlüsselt werden', entries: [
|
{ ok: true, warningCode: 'KEYRING_DECRYPT_FAILED', warning: 'Gespeicherter Online-Sicherungsschlüssel konnte nicht entschlüsselt werden', entries: [
|
||||||
@@ -107,8 +108,8 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
const resolve = pendingManagedOnlineBackupLists.get(index);
|
const resolve = pendingManagedOnlineBackupLists.get(index);
|
||||||
pendingManagedOnlineBackupLists.delete(index);
|
pendingManagedOnlineBackupLists.delete(index);
|
||||||
resolve({ ok: true, entries: [
|
resolve({ ok: true, entries: [
|
||||||
|
{ id: managedOnlineBackupIds.e, displayKey: 'MHU2-AUTH…9999', createdAt: '2026-08-26T10:00:00.000Z' },
|
||||||
{ id: managedOnlineBackupIds.d, displayKey: 'MHU2-DFGH…2468', createdAt: '2026-08-25T10:00:00.000Z' },
|
{ id: managedOnlineBackupIds.d, displayKey: 'MHU2-DFGH…2468', createdAt: '2026-08-25T10:00:00.000Z' },
|
||||||
{ id: managedOnlineBackupIds.c, displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' },
|
|
||||||
{ id: managedOnlineBackupIds.a, displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' }
|
{ id: managedOnlineBackupIds.a, displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' }
|
||||||
] });
|
] });
|
||||||
},
|
},
|
||||||
@@ -146,7 +147,8 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||||
b: 'AQEBAQEBAQEBAQEBAQEBAQ',
|
b: 'AQEBAQEBAQEBAQEBAQEBAQ',
|
||||||
c: 'AgICAgICAgICAgICAgICAg',
|
c: 'AgICAgICAgICAgICAgICAg',
|
||||||
d: 'AwMDAwMDAwMDAwMDAwMDAw'
|
d: 'AwMDAwMDAwMDAwMDAwMDAw',
|
||||||
|
e: 'BAQEBAQEBAQEBAQEBAQEBA'
|
||||||
};
|
};
|
||||||
const fixture = document.createElement('section');
|
const fixture = document.createElement('section');
|
||||||
fixture.innerHTML = '<div id="managedOnlineBackupList"></div><div id="managedOnlineBackupRefreshStatus" hidden><span id="managedOnlineBackupRefreshMessage"></span><button id="reloadManagedOnlineBackupsBtn" type="button">Erneut laden</button></div><div id="onlineBackupStatus"></div><button id="createOnlineBackupBtn"></button><input id="onlineBackupKeyInput"><button id="restoreOnlineBackupBtn"></button>';
|
fixture.innerHTML = '<div id="managedOnlineBackupList"></div><div id="managedOnlineBackupRefreshStatus" hidden><span id="managedOnlineBackupRefreshMessage"></span><button id="reloadManagedOnlineBackupsBtn" type="button">Erneut laden</button></div><div id="onlineBackupStatus"></div><button id="createOnlineBackupBtn"></button><input id="onlineBackupKeyInput"><button id="restoreOnlineBackupBtn"></button>';
|
||||||
@@ -168,6 +170,8 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
};
|
};
|
||||||
flushPendingSettingsSaves = async () => {};
|
flushPendingSettingsSaves = async () => {};
|
||||||
openOnlineBackupView = () => {};
|
openOnlineBackupView = () => {};
|
||||||
|
const toastMessages = [];
|
||||||
|
showCopyToast = message => toastMessages.push(localizeUiText(message));
|
||||||
await loadManagedOnlineBackups();
|
await loadManagedOnlineBackups();
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
const initialKeys = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent);
|
const initialKeys = [...document.querySelectorAll('.online-backup-managed-key')].map(element => element.textContent);
|
||||||
@@ -198,6 +202,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
focusAction: document.activeElement?.dataset.managedOnlineBackupAction,
|
focusAction: document.activeElement?.dataset.managedOnlineBackupAction,
|
||||||
focusId: document.activeElement?.dataset.managedOnlineBackupId
|
focusId: document.activeElement?.dataset.managedOnlineBackupId
|
||||||
};
|
};
|
||||||
|
const beforeCreateToasts = toastMessages.length;
|
||||||
await doOnlineBackupCreate();
|
await doOnlineBackupCreate();
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
const refreshFailure = {
|
const refreshFailure = {
|
||||||
@@ -207,6 +212,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
warning: document.getElementById('managedOnlineBackupRefreshMessage')?.textContent,
|
warning: document.getElementById('managedOnlineBackupRefreshMessage')?.textContent,
|
||||||
retryVisible: document.getElementById('reloadManagedOnlineBackupsBtn')?.offsetParent !== null
|
retryVisible: document.getElementById('reloadManagedOnlineBackupsBtn')?.offsetParent !== null
|
||||||
};
|
};
|
||||||
|
const createSuccessToasts = toastMessages.slice(beforeCreateToasts);
|
||||||
const beforeRetryCalls = (await calls('list')).length;
|
const beforeRetryCalls = (await calls('list')).length;
|
||||||
document.getElementById('reloadManagedOnlineBackupsBtn')?.click();
|
document.getElementById('reloadManagedOnlineBackupsBtn')?.click();
|
||||||
const retryTriggeredLoad = await waitFor(async () => (await calls('list')).length === beforeRetryCalls + 1);
|
const retryTriggeredLoad = await waitFor(async () => (await calls('list')).length === beforeRetryCalls + 1);
|
||||||
@@ -251,6 +257,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
ariaDescriptions,
|
ariaDescriptions,
|
||||||
afterDelete,
|
afterDelete,
|
||||||
refreshFailure,
|
refreshFailure,
|
||||||
|
createSuccessToasts,
|
||||||
retryTriggeredLoad,
|
retryTriggeredLoad,
|
||||||
afterRetry,
|
afterRetry,
|
||||||
copyFocusRestored,
|
copyFocusRestored,
|
||||||
@@ -418,6 +425,7 @@ app.whenReady().then(async () => {
|
|||||||
warning: 'Stored online backup key could not be decrypted',
|
warning: 'Stored online backup key could not be decrypted',
|
||||||
retryVisible: true
|
retryVisible: true
|
||||||
});
|
});
|
||||||
|
assert.deepEqual(result.onlineBackupBehavior.createSuccessToasts, []);
|
||||||
assert.equal(result.onlineBackupBehavior.retryTriggeredLoad, true);
|
assert.equal(result.onlineBackupBehavior.retryTriggeredLoad, true);
|
||||||
assert.deepEqual(result.onlineBackupBehavior.afterRetry, {
|
assert.deepEqual(result.onlineBackupBehavior.afterRetry, {
|
||||||
keys: ['MHU2-QWER…4321', 'MHU2-ABCD…1234'],
|
keys: ['MHU2-QWER…4321', 'MHU2-ABCD…1234'],
|
||||||
@@ -425,7 +433,7 @@ app.whenReady().then(async () => {
|
|||||||
});
|
});
|
||||||
assert.equal(result.onlineBackupBehavior.copyFocusRestored, true);
|
assert.equal(result.onlineBackupBehavior.copyFocusRestored, true);
|
||||||
assert.deepEqual(result.onlineBackupBehavior.raceResult, {
|
assert.deepEqual(result.onlineBackupBehavior.raceResult, {
|
||||||
keys: ['MHU2-DFGH…2468', 'MHU2-QWER…4321', 'MHU2-ABCD…1234'],
|
keys: ['MHU2-AUTH…9999', 'MHU2-DFGH…2468', 'MHU2-ABCD…1234'],
|
||||||
status: 'New key created.',
|
status: 'New key created.',
|
||||||
statusState: 'success',
|
statusState: 'success',
|
||||||
warningHidden: true
|
warningHidden: true
|
||||||
@@ -523,7 +531,13 @@ const serverModulePath = process.env.MHU_DPAPI_SERVER_MODULE;
|
|||||||
app.setPath('userData', userDataPath);
|
app.setPath('userData', userDataPath);
|
||||||
let server = null;
|
let server = null;
|
||||||
let window = null;
|
let window = null;
|
||||||
let previousClipboard = '';
|
let interceptedClipboard = '';
|
||||||
|
let interceptedClipboardWrites = 0;
|
||||||
|
const originalClipboardWriteText = clipboard.writeText;
|
||||||
|
clipboard.writeText = value => {
|
||||||
|
interceptedClipboard = value;
|
||||||
|
interceptedClipboardWrites++;
|
||||||
|
};
|
||||||
const logs = [];
|
const logs = [];
|
||||||
const originalConsole = { log: console.log, warn: console.warn, error: console.error };
|
const originalConsole = { log: console.log, warn: console.warn, error: console.error };
|
||||||
for (const method of Object.keys(originalConsole)) console[method] = (...values) => { logs.push(values.map(String).join(' ')); };
|
for (const method of Object.keys(originalConsole)) console[method] = (...values) => { logs.push(values.map(String).join(' ')); };
|
||||||
@@ -545,7 +559,6 @@ async function closeServer() {
|
|||||||
server = null;
|
server = null;
|
||||||
}
|
}
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
previousClipboard = clipboard.readText();
|
|
||||||
if (!safeStorage.isEncryptionAvailable()) throw new Error('safeStorage unavailable');
|
if (!safeStorage.isEncryptionAvailable()) throw new Error('safeStorage unavailable');
|
||||||
const { createBackupServer } = await import(pathToFileURL(serverModulePath).href);
|
const { createBackupServer } = await import(pathToFileURL(serverModulePath).href);
|
||||||
fs.mkdirSync(serverDataPath, { recursive: true });
|
fs.mkdirSync(serverDataPath, { recursive: true });
|
||||||
@@ -597,7 +610,7 @@ app.whenReady().then(async () => {
|
|||||||
ipcMain.handle('online-backup:copy-managed', async (event, id) => {
|
ipcMain.handle('online-backup:copy-managed', async (event, id) => {
|
||||||
if (!trusted(event)) return { ok: false, error: 'rejected' };
|
if (!trusted(event)) return { ok: false, error: 'rejected' };
|
||||||
const result = await manager.copyManaged(requireId(id));
|
const result = await manager.copyManaged(requireId(id));
|
||||||
clipboardMatched = clipboard.readText() === fullKey;
|
clipboardMatched = interceptedClipboard === fullKey && interceptedClipboardWrites === 1;
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
ipcMain.handle('online-backup:delete-managed', (event, id) => trusted(event) ? manager.deleteManaged(requireId(id)) : { ok: false, error: 'rejected' });
|
ipcMain.handle('online-backup:delete-managed', (event, id) => trusted(event) ? manager.deleteManaged(requireId(id)) : { ok: false, error: 'rejected' });
|
||||||
@@ -635,6 +648,7 @@ app.whenReady().then(async () => {
|
|||||||
rendererSecretAbsent: !ipcJson.includes(fullKey) && !/MHU2-[A-Za-z0-9_-]{70}/.test(ipcJson),
|
rendererSecretAbsent: !ipcJson.includes(fullKey) && !/MHU2-[A-Za-z0-9_-]{70}/.test(ipcJson),
|
||||||
logSecretAbsent: logs.every(line => !line.includes(fullKey)) && !logs.join(' ').match(/MHU2-[A-Za-z0-9_-]{70}/),
|
logSecretAbsent: logs.every(line => !line.includes(fullKey)) && !logs.join(' ').match(/MHU2-[A-Za-z0-9_-]{70}/),
|
||||||
clipboardMatched,
|
clipboardMatched,
|
||||||
|
interceptedClipboardWrites,
|
||||||
serverRecordBeforeDelete,
|
serverRecordBeforeDelete,
|
||||||
serverEmptyAfterDelete: remainingServerRecords.length === 0,
|
serverEmptyAfterDelete: remainingServerRecords.length === 0,
|
||||||
keyringEmptyAfterDelete: afterDocument.keys.length === 0,
|
keyringEmptyAfterDelete: afterDocument.keys.length === 0,
|
||||||
@@ -645,12 +659,7 @@ app.whenReady().then(async () => {
|
|||||||
fs.writeFileSync(outputPath, JSON.stringify({ error: error.stack || String(error) }), 'utf8');
|
fs.writeFileSync(outputPath, JSON.stringify({ error: error.stack || String(error) }), 'utf8');
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}).finally(async () => {
|
}).finally(async () => {
|
||||||
clipboard.writeText(previousClipboard);
|
clipboard.writeText = originalClipboardWriteText;
|
||||||
if (fs.existsSync(outputPath)) {
|
|
||||||
const result = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
|
||||||
result.clipboardRestored = clipboard.readText() === previousClipboard;
|
|
||||||
fs.writeFileSync(outputPath, JSON.stringify(result), 'utf8');
|
|
||||||
}
|
|
||||||
for (const channel of ['online-backup:list-managed', 'online-backup:create-managed', 'online-backup:copy-managed', 'online-backup:delete-managed']) ipcMain.removeHandler(channel);
|
for (const channel of ['online-backup:list-managed', 'online-backup:create-managed', 'online-backup:copy-managed', 'online-backup:delete-managed']) ipcMain.removeHandler(channel);
|
||||||
if (window && !window.isDestroyed()) window.destroy();
|
if (window && !window.isDestroyed()) window.destroy();
|
||||||
await closeServer();
|
await closeServer();
|
||||||
@@ -696,7 +705,7 @@ app.whenReady().then(async () => {
|
|||||||
rendererSecretAbsent: true,
|
rendererSecretAbsent: true,
|
||||||
logSecretAbsent: true,
|
logSecretAbsent: true,
|
||||||
clipboardMatched: true,
|
clipboardMatched: true,
|
||||||
clipboardRestored: true,
|
interceptedClipboardWrites: 1,
|
||||||
serverRecordBeforeDelete: true,
|
serverRecordBeforeDelete: true,
|
||||||
serverEmptyAfterDelete: true,
|
serverEmptyAfterDelete: true,
|
||||||
keyringEmptyAfterDelete: true,
|
keyringEmptyAfterDelete: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user