release: v2.1.12 reliability and security hardening
CI / verify (push) Waiting to run

Verify update artifacts with exact metadata and SHA-512, require host-confirmed upload completion before cleanup, harden credentials and backups, improve queue recovery and skipped-state reporting, expand Windows path coverage, and add CI packaging checks.
This commit is contained in:
Sucukdeluxe
2026-08-11 22:36:21 +02:00
parent 2c124c848c
commit 2ba0106ef0
47 changed files with 2069 additions and 1192 deletions
+92 -14
View File
@@ -3,9 +3,10 @@ const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const os = require('node:os');
const crypto = require('node:crypto');
const { pathToFileURL } = require('node:url');
const { isNewer, resolveReleaseVersion, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate } = require('../lib/updater');
const { isNewer, resolveReleaseVersion, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate, pickSetupAsset, parseLatestYml } = require('../lib/updater');
const releasePlanUrl = pathToFileURL(path.resolve(__dirname, '../scripts/release-plan.mjs')).href;
test('bridge title resolves product version instead of transport tag', () => {
@@ -53,21 +54,28 @@ test('update preparation writes a verified installer without launching it', asyn
assetUrl: 'https://update.invalid/setup.exe',
assetName: 'setup.exe',
assetSize: installer.length,
latestYmlUrl: null
remoteVersion: '2.2.0',
latestYmlUrl: 'https://update.invalid/latest.yml'
},
tempDir,
fetchImpl: async () => ({
ok: true,
status: 200,
body: {
getReader: () => ({
read: async () => {
reads++;
return reads === 1 ? { done: false, value: installer } : { done: true };
fetchImpl: async url => url.endsWith('latest.yml')
? {
ok: true,
status: 200,
text: async () => `version: 2.2.0\npath: setup.exe\nsha512: ${crypto.createHash('sha512').update(installer).digest('base64')}\nsize: ${installer.length}\n`
}
: {
ok: true,
status: 200,
body: {
getReader: () => ({
read: async () => {
reads++;
return reads === 1 ? { done: false, value: installer } : { done: true };
}
})
}
})
}
})
}
});
assert.equal(prepared.installerPath, path.join(tempDir, 'setup.exe'));
@@ -78,6 +86,76 @@ test('update preparation writes a verified installer without launching it', asyn
}
});
test('update preparation fails closed when checksum metadata is unavailable', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-updater-test-'));
try {
await assert.rejects(
prepareUpdate(null, {
checkResult: {
available: true,
assetUrl: 'https://update.invalid/setup.exe',
assetName: 'setup.exe',
assetSize: 128 * 1024,
remoteVersion: '2.2.0',
latestYmlUrl: null
},
tempDir,
fetchImpl: async () => assert.fail('installer download must not start without checksum metadata')
}),
/Prüfsummen-Metadaten fehlen/
);
assert.equal(fs.existsSync(path.join(tempDir, 'setup.exe')), false);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test('setup selection never falls back to a portable executable', () => {
const portable = { name: 'Multi-Hoster-Upload 2.2.0.exe' };
assert.equal(pickSetupAsset([portable], '2.2.0'), null);
assert.deepEqual(
pickSetupAsset([portable, { name: 'Multi-Hoster-Upload Setup 2.2.0.exe' }], '2.2.0'),
{ name: 'Multi-Hoster-Upload Setup 2.2.0.exe' }
);
});
test('checksum metadata must match the selected version, installer name, size, and SHA-512 shape', async () => {
const sha = crypto.randomBytes(64).toString('base64');
const fetchImpl = async () => ({
ok: true,
status: 200,
text: async () => `version: 2.2.0\npath: Multi-Hoster-Upload Setup 2.2.0.exe\nsha512: ${sha}\nsize: 456\n`
});
const metadata = await parseLatestYml('https://update.invalid/latest.yml', {
version: '2.2.0',
assetName: 'Multi-Hoster-Upload Setup 2.2.0.exe',
assetSize: 456
}, fetchImpl);
assert.deepEqual(metadata, {
version: '2.2.0',
path: 'Multi-Hoster-Upload Setup 2.2.0.exe',
size: 456,
sha512: sha
});
});
test('checksum metadata rejects a path that belongs to another artifact', async () => {
const sha = crypto.randomBytes(64).toString('base64');
await assert.rejects(
parseLatestYml('https://update.invalid/latest.yml', {
version: '2.2.0',
assetName: 'Multi-Hoster-Upload Setup 2.2.0.exe',
assetSize: 456
}, async () => ({
ok: true,
status: 200,
text: async () => `version: 2.2.0\npath: Multi-Hoster-Upload 2.2.0.exe\nsha512: ${sha}\nsize: 456\n`
})),
/gehören nicht zum ausgewählten Installer/
);
});
test('a prepared installer launches at most once', () => {
const calls = [];
const child = { unrefCalls: 0, unref() { this.unrefCalls++; } };
@@ -120,7 +198,7 @@ test('release plan keeps product artifacts separate from the transport tag', asy
'Multi-Hoster-Upload Setup 2.0.7.exe.blockmap',
'latest.yml'
],
latestYml: "version: 2.0.7\nfiles:\n - url: Multi-Hoster-Upload.Setup.2.0.7.exe\n sha512: abc123\n size: 456\npath: Multi-Hoster-Upload.Setup.2.0.7.exe\nsha512: abc123\nreleaseDate: '2026-08-07T12:00:00.000Z'\n"
latestYml: "version: 2.0.7\nfiles:\n - url: Multi-Hoster-Upload Setup 2.0.7.exe\n sha512: abc123\n size: 456\npath: Multi-Hoster-Upload Setup 2.0.7.exe\nsha512: abc123\nreleaseDate: '2026-08-07T12:00:00.000Z'\n"
});
});