Comprehensive bugfix release v1.6.45

Fix ~70 issues across the entire codebase including security fixes,
error handling improvements, test stabilization, and code quality.

- Fix TLS race condition with reference-counted acquire/release
- Bind debug server to 127.0.0.1 instead of 0.0.0.0
- Add overall timeout to MegaWebFallback
- Stream update installer to disk instead of RAM buffering
- Add path traversal protection in JVM extractor
- Cache DdownloadClient with credential-based invalidation
- Add .catch() to all fire-and-forget IPC calls
- Wrap app startup, clipboard, session-log in try/catch
- Add timeouts to container.ts fetch calls
- Fix variable shadowing, tsconfig path, line endings
- Stabilize tests with proper cleanup and timing tolerance
- Fix installer privileges, scripts, and afterPack null checks
- Delete obsolete _upload_release.mjs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-03-05 03:53:28 +01:00
co-authored by Claude Opus 4.6
parent a3c2680fec
commit a1c8f42435
29 changed files with 381 additions and 272 deletions
+11 -2
View File
@@ -2,8 +2,17 @@ const path = require("path");
const { rcedit } = require("rcedit");
module.exports = async function afterPack(context) {
const exePath = path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.exe`);
const productFilename = context.packager?.appInfo?.productFilename;
if (!productFilename) {
console.warn(" • rcedit: skipped — productFilename not available");
return;
}
const exePath = path.join(context.appOutDir, `${productFilename}.exe`);
const iconPath = path.resolve(__dirname, "..", "assets", "app_icon.ico");
console.log(` • rcedit: patching icon → ${exePath}`);
await rcedit(exePath, { icon: iconPath });
try {
await rcedit(exePath, { icon: iconPath });
} catch (error) {
console.warn(` • rcedit: failed — ${String(error)}`);
}
};
+14 -11
View File
@@ -31,18 +31,21 @@ async function main(): Promise<void> {
login: settings.megaLogin,
password: settings.megaPassword
}));
const service = new DebridService(settings, {
megaWebUnrestrict: (link) => megaWeb.unrestrict(link)
});
for (const link of links) {
try {
const result = await service.unrestrictLink(link);
console.log(`[OK] ${result.providerLabel} -> ${result.fileName}`);
} catch (error) {
console.log(`[FAIL] ${String(error)}`);
try {
const service = new DebridService(settings, {
megaWebUnrestrict: (link) => megaWeb.unrestrict(link)
});
for (const link of links) {
try {
const result = await service.unrestrictLink(link);
console.log(`[OK] ${result.providerLabel} -> ${result.fileName}`);
} catch (error) {
console.log(`[FAIL] ${String(error)}`);
}
}
} finally {
megaWeb.dispose();
}
megaWeb.dispose();
}
void main();
main().catch(e => { console.error(e); process.exit(1); });
+12 -3
View File
@@ -16,8 +16,8 @@ function sleep(ms) {
}
function cookieFrom(headers) {
const raw = headers.get("set-cookie") || "";
return raw.split(",").map((x) => x.split(";")[0].trim()).filter(Boolean).join("; ");
const cookies = headers.getSetCookie();
return cookies.map((x) => x.split(";")[0].trim()).filter(Boolean).join("; ");
}
function parseDebridCodes(html) {
@@ -47,6 +47,9 @@ async function resolveCode(cookie, code) {
});
const text = (await res.text()).trim();
if (text === "reload") {
if (attempt % 5 === 0) {
console.log(` [retry] code=${code} attempt=${attempt}/50 (waiting for server)`);
}
await sleep(800);
continue;
}
@@ -98,7 +101,13 @@ async function main() {
redirect: "manual"
});
if (loginRes.status >= 400) {
throw new Error(`Login failed with HTTP ${loginRes.status}`);
}
const cookie = cookieFrom(loginRes.headers);
if (!cookie) {
throw new Error("Login returned no session cookie");
}
console.log("login", loginRes.status, loginRes.headers.get("location") || "");
const debridRes = await fetch("https://www.mega-debrid.eu/index.php?form=debrid", {
@@ -136,4 +145,4 @@ async function main() {
}
}
await main();
await main().catch((e) => { console.error(e); process.exit(1); });
+8 -4
View File
@@ -66,6 +66,8 @@ async function callRealDebrid(link) {
};
}
// megaCookie is intentionally cached at module scope so that multiple
// callMegaDebrid() invocations reuse the same session cookie.
async function callMegaDebrid(link) {
if (!megaCookie) {
const loginRes = await fetch("https://www.mega-debrid.eu/index.php?form=login", {
@@ -77,13 +79,15 @@ async function callMegaDebrid(link) {
body: new URLSearchParams({ login: megaLogin, password: megaPassword, remember: "on" }),
redirect: "manual"
});
megaCookie = (loginRes.headers.get("set-cookie") || "")
.split(",")
if (loginRes.status >= 400) {
return { ok: false, error: `Mega-Web login failed with HTTP ${loginRes.status}` };
}
megaCookie = loginRes.headers.getSetCookie()
.map((chunk) => chunk.split(";")[0].trim())
.filter(Boolean)
.join("; ");
if (!megaCookie) {
return { ok: false, error: "Mega-Web login failed" };
return { ok: false, error: "Mega-Web login returned no session cookie" };
}
}
@@ -290,4 +294,4 @@ async function main() {
}
}
await main();
await main().catch((e) => { console.error(e); process.exit(1); });
+39 -12
View File
@@ -37,7 +37,8 @@ function runWithInput(command, args, input) {
cwd: process.cwd(),
encoding: "utf8",
input,
stdio: ["pipe", "pipe", "pipe"]
stdio: ["pipe", "pipe", "pipe"],
timeout: 10000
});
if (result.status !== 0) {
const stderr = String(result.stderr || "").trim();
@@ -95,15 +96,17 @@ function getGiteaRepo() {
const preferredBase = normalizeBaseUrl(process.env.GITEA_BASE_URL || process.env.FORGEJO_BASE_URL || "https://git.24-music.de");
const preferredProtocol = preferredBase ? new URL(preferredBase).protocol : "https:";
for (const remote of remotes) {
try {
const remoteUrl = runCapture("git", ["remote", "get-url", remote]);
const parsed = parseRemoteUrl(remoteUrl);
const remoteBase = `https://${parsed.host}`.toLowerCase();
if (preferredBase && remoteBase !== preferredBase.toLowerCase()) {
if (preferredBase && remoteBase !== preferredBase.toLowerCase().replace(/^http:/, "https:")) {
continue;
}
return { remote, ...parsed, baseUrl: `https://${parsed.host}` };
return { remote, ...parsed, baseUrl: `${preferredProtocol}//${parsed.host}` };
} catch {
// try next remote
}
@@ -179,7 +182,8 @@ function updatePackageVersion(rootDir, version) {
const packagePath = path.join(rootDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
if (String(packageJson.version || "") === version) {
throw new Error(`package.json is already at version ${version}`);
process.stdout.write(`package.json is already at version ${version}, skipping update.\n`);
return;
}
packageJson.version = version;
fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
@@ -257,9 +261,31 @@ async function createOrGetRelease(baseApi, tag, authHeader, notes) {
async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, files) {
for (const fileName of files) {
const filePath = path.join(releaseDir, fileName);
const fileData = fs.readFileSync(filePath);
const fileSize = fs.statSync(filePath).size;
const uploadUrl = `${baseApi}/releases/${releaseId}/assets?name=${encodeURIComponent(fileName)}`;
const response = await apiRequest("POST", uploadUrl, authHeader, fileData, "application/octet-stream");
// Stream large files instead of loading them entirely into memory
const fileStream = fs.createReadStream(filePath);
const response = await fetch(uploadUrl, {
method: "POST",
headers: {
Accept: "application/json",
Authorization: authHeader,
"Content-Type": "application/octet-stream",
"Content-Length": String(fileSize)
},
body: fileStream,
duplex: "half"
});
const text = await response.text();
let parsed;
try {
parsed = text ? JSON.parse(text) : null;
} catch {
parsed = text;
}
if (response.ok) {
process.stdout.write(`Uploaded: ${fileName}\n`);
continue;
@@ -268,7 +294,7 @@ async function uploadReleaseAssets(baseApi, releaseId, authHeader, releaseDir, f
process.stdout.write(`Skipped existing asset: ${fileName}\n`);
continue;
}
throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(response.body)}`);
throw new Error(`Asset upload failed for ${fileName} (${response.status}): ${JSON.stringify(parsed)}`);
}
}
@@ -290,17 +316,18 @@ async function main() {
ensureNoTrackedChanges();
ensureTagMissing(tag);
if (args.dryRun) {
process.stdout.write(`Dry run: would release ${tag}. No changes made.\n`);
return;
}
updatePackageVersion(rootDir, version);
process.stdout.write(`Building release artifacts for ${tag}...\n`);
run(NPM_EXECUTABLE, ["run", "release:win"]);
const assets = ensureAssetsExist(rootDir, version);
if (args.dryRun) {
process.stdout.write(`Dry run complete. Assets exist for ${tag}.\n`);
return;
}
run("git", ["add", "package.json"]);
run("git", ["commit", "-m", `Release ${tag}`]);
run("git", ["push", repo.remote, "main"]);