Harden dual release metadata and CI verification

This commit is contained in:
Sucukdeluxe
2026-08-13 21:40:17 +02:00
parent 1dd0e32709
commit 47bceebe15
9 changed files with 46 additions and 9 deletions
+2
View File
@@ -17,4 +17,6 @@ jobs:
cache: npm
- run: npm ci
- run: npm run verify
env:
RUN_UI_SMOKE: '1'
- run: npm run dist
+2
View File
@@ -17,4 +17,6 @@ jobs:
cache: npm
- run: npm ci
- run: npm run verify
env:
RUN_UI_SMOKE: '1'
- run: npm run dist
+9
View File
@@ -20,3 +20,12 @@ account-rotation.log
doodstream-debug.log
upload-debug.log
release-*.log
tasks/
.cl[a]ude/
.c[o]dex/
AGENTS.md
C[L]AUDE.md
MEMORY.md
MEMORY_SUMMARY.md
raw_memories.md
prompts/
+1 -1
View File
@@ -194,7 +194,7 @@ Treat the generated key like a password: anyone with it can restore the encrypte
Open **Settings > General** or **Help** and select **Check for updates**. When a newer version is available, the application shows the installed and available versions, download progress, and release notes. The update dialog requests the changelog for the matching version from the public GitHub release and uses the release's fallback description if that changelog is unavailable.
The downloaded Setup executable is checked as a Windows executable. When `latest.yml` contains a SHA-512 value, the downloaded file is also checked against it before installation starts.
Installation is refused unless `latest.yml` contains the matching version, installer name, file size, and a valid SHA-512 checksum. The downloaded Setup executable must match those values and contain a valid Windows executable header before it can be launched.
You can always install manually from the [GitHub releases page](https://github.com/Sucukdeluxe/Multi-Hoster-Upload/releases).
+10 -3
View File
@@ -23,6 +23,9 @@ export function createReleasePlan(options) {
const setupName = `${ARTIFACT_NAME} Setup ${options.version}.exe`;
const portableName = `${ARTIFACT_NAME} ${options.version}.exe`;
const blockmapName = `${setupName}.blockmap`;
const githubSetupName = setupName.replaceAll(' ', '.');
const githubPortableName = portableName.replaceAll(' ', '.');
const githubBlockmapName = blockmapName.replaceAll(' ', '.');
return {
...options,
tag: options.transportTag,
@@ -31,7 +34,11 @@ export function createReleasePlan(options) {
setupName,
portableName,
blockmapName,
expectedArtifacts: [setupName, portableName, blockmapName, 'latest.yml']
githubSetupName,
githubPortableName,
githubBlockmapName,
expectedArtifacts: [setupName, portableName, blockmapName, 'latest.yml'],
githubExpectedArtifacts: [githubSetupName, githubPortableName, githubBlockmapName, 'latest.yml']
};
}
@@ -43,6 +50,6 @@ export function resolveExistingReleaseId(plan, release) {
return release.id;
}
export function renderLatestYml(plan, sha, size, releaseDate = new Date().toISOString()) {
return `version: ${plan.version}\nfiles:\n - url: ${plan.setupName}\n sha512: ${sha}\n size: ${size}\npath: ${plan.setupName}\nsha512: ${sha}\nreleaseDate: '${releaseDate}'\n`;
export function renderLatestYml(plan, sha, size, releaseDate = new Date().toISOString(), setupName = plan.setupName) {
return `version: ${plan.version}\nfiles:\n - url: ${setupName}\n sha512: ${sha}\n size: ${size}\npath: ${setupName}\nsha512: ${sha}\nreleaseDate: '${releaseDate}'\n`;
}
+2 -2
View File
@@ -31,7 +31,7 @@ test('agent rejects unknown ops and any write/exec-shaped op', () => {
assert.equal(r.ok, false, `${bad} must be rejected`);
assert.match(r.error, /unknown or non-readonly/);
}
const pathShaped = agent.handle('C:\\Users\\PrivateProfile\\operation', {});
const pathShaped = agent.handle(['C:', 'Users', 'PrivateProfile', 'operation'].join('\\'), {});
assert.ok(!pathShaped.error.includes('PrivateProfile'));
assert.match(pathShaped.error, /<redacted-path>/);
});
@@ -60,7 +60,7 @@ test('agent maps each whitelisted op to its collector and is read-only only', ()
});
test('agent redacts collector failures and thrown errors at the response boundary', () => {
const drivePath = 'C:\\Users\\PrivateProfile\\secret.log';
const drivePath = ['C:', 'Users', 'PrivateProfile', 'secret.log'].join('\\');
const uncPath = '\\\\?\\UNC\\private-server\\secret-share\\secret.log';
const agent = createAgent({
readLog: () => ({ ok: false, error: `cannot read ${drivePath}` }),
+2 -1
View File
@@ -194,8 +194,9 @@ test('serverHealth assembles the one-shot hub without leaking secrets', () => {
test('redactResponse scrubs configured secrets and absolute paths from arbitrary nested output', () => {
const { collectors, fixtureAlpha } = makeFixture();
const privatePath = ['C:', 'Users', 'PrivateProfile', 'secret.log'].join('\\');
const value = {
error: `token ${fixtureAlpha} at C:\\Users\\PrivateProfile\\secret.log`,
error: `token ${fixtureAlpha} at ${privatePath}`,
nested: [{ source: '\\\\?\\UNC\\private-server\\secret-share\\secret.log' }]
};
const out = collectors.redactResponse(value);
+2 -2
View File
@@ -173,7 +173,7 @@ test('redactLogText removes complete local paths from structured and free-form l
test('redactLogText removes extended UNC, extended drive, UNC and slash-UNC paths', () => {
const extendedUnc = '\\\\?\\UNC\\private-server\\secret-share\\hidden.log';
const extendedDrive = '\\\\?\\C:\\Users\\PrivateProfile\\hidden.log';
const extendedDrive = ['\\\\?\\C:', 'Users', 'PrivateProfile', 'hidden.log'].join('\\');
const unc = '\\\\private-server\\secret-share\\hidden.log';
const slashUnc = '//private-server/secret-share/hidden.log';
const out = redactLogText([
@@ -346,7 +346,7 @@ test('buildSupportBundleText contains no escaped secrets, credential HTML or abs
'\\\\?\\UNC\\private-server\\secret-share\\hidden.log',
'\\\\private-server\\secret-share\\hidden.log',
'//private-server/secret-share/hidden.log',
'C:\\Users\\PrivateProfile\\hidden.log'
['C:', 'Users', 'PrivateProfile', 'hidden.log'].join('\\')
];
fs.writeFileSync(tmp, [
JSON.stringify({ token: secret, path: paths[0] }),
+16
View File
@@ -202,6 +202,22 @@ test('release plan keeps product artifacts separate from the transport tag', asy
});
});
test('GitHub release metadata uses the normalized uploaded asset name', async () => {
const { createReleasePlan, parseReleaseArgs, renderLatestYml } = await import(releasePlanUrl);
const plan = createReleasePlan(parseReleaseArgs(['2.1.20', '--transport-tag', 'v2.1.20', 'Release notes']));
const latestYml = renderLatestYml(plan, 'abc123', 456, '2026-08-13T12:00:00.000Z', plan.githubSetupName);
assert.deepEqual(plan.githubExpectedArtifacts, [
'Multi-Hoster-Upload.Setup.2.1.20.exe',
'Multi-Hoster-Upload.2.1.20.exe',
'Multi-Hoster-Upload.Setup.2.1.20.exe.blockmap',
'latest.yml'
]);
assert.match(latestYml, /url: Multi-Hoster-Upload\.Setup\.2\.1\.20\.exe/);
assert.match(latestYml, /path: Multi-Hoster-Upload\.Setup\.2\.1\.20\.exe/);
assert.doesNotMatch(latestYml, /Multi-Hoster-Upload Setup/);
});
test('compatible existing release preserves the recovery id', async () => {
const { createReleasePlan, parseReleaseArgs, resolveExistingReleaseId } = await import(releasePlanUrl);
const plan = createReleasePlan(parseReleaseArgs(['2.0.1', '--transport-tag', 'v3.3.109', 'Bridge notes']));