Fix: Mega-Debrid Web-Rotation nutzt jetzt die Per-Account-Credentials (echter Rotations-Bug)

Root-Cause (verifiziert via Support-Bundle): Der Web-Unrestrict lief fuer JEDEN rotierten
Account mit den Creds des ersten/Legacy-Accounts (settings.megaLogin), weil MegaWebFallback
EINE geteilte Cookie-Session + festes getCredentials() nutzte UND megaWebUnrestrict ohne
Account-Bezug aufgerufen wurde. Item-Log-Beweis: "Account 2/2 (FabelDavid): Mega-Web Antwort
leer", obwohl FabelDavid real funktioniert — die Rotation nutzte FabelDavid nie wirklich,
sondern immer Account 1 (am Limit). Alle bisherigen Fixes (v1.7.169-172) lagen downstream
dieses Punkts und konnten den Bug nicht beheben.

Fix:
- MegaWebUnrestrictor bekommt optionalen `account`-Parameter; MegaDebridClient.unrestrictViaWeb
  reicht this.login/this.password (den rotierten Account) durch; app-controller leitet ihn weiter.
- MegaWebFallback: Per-Login Session-Cache (Map<login,{cookie,setAt}>) statt einem geteilten
  Cookie; login() gibt das Cookie zurueck, generate() bekommt es als Param. Jeder Account nutzt
  seine eigene Session — kein Re-Login-Thrash unter Parallel-Last (maxParallel=8).

Tests: mega-web-fallback (Login-POST traegt den uebergebenen Account-Login, nicht den Default)
+ debrid-Rotation (jeder Account erhaelt SEINE Creds; Account 2 loest auf). 647 Tests gruen,
tsc 9, Build sauber.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-05-31 19:59:37 +02:00
co-authored by Claude Opus 4.8
parent fd2cb724a3
commit 0be5248a36
5 changed files with 121 additions and 33 deletions
+44
View File
@@ -1522,6 +1522,50 @@ describe("debrid service", () => {
expect(calls).toBe(2);
}, 20000);
it("passes each account's OWN credentials to the Mega web unrestrict during rotation", async () => {
// Echter Root-Cause (Support-Bundle): der Web-Pfad nutzte fuer JEDEN rotierten
// Account die Creds des ersten/Legacy-Accounts → "Account 2" lief in Wahrheit mit
// Account-1-Login → der zweite (funktionierende) Account wurde nie verwendet.
// Jetzt muss jeder Account-Versuch SEINE eigenen Creds an den Web-Unrestrict reichen.
const settings = {
...defaultSettings(),
token: "",
bestToken: "",
allDebridToken: "",
megaLogin: "user1",
megaPassword: "pass1",
megaCredentials: "user1:pass1\nuser2:pass2",
megaDebridPreferApi: false,
providerOrder: [] as const,
providerPrimary: "megadebrid" as const,
providerSecondary: "none" as const,
providerTertiary: "none" as const,
autoProviderFallback: false
};
globalThis.fetch = (async () => new Response("error", { status: 500 })) as typeof fetch;
const accountsSeen: Array<string | undefined> = [];
const megaWeb = vi.fn(async (_link: string, _signal: AbortSignal | undefined, account?: { login: string; password: string }) => {
accountsSeen.push(account?.login);
if (account?.login === "user1") {
// Account 1 am Tageslimit.
throw new Error("Mega-Web: Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal.");
}
// Account 2 (eigene Creds) loest auf.
return { fileName: "ok.rar", directUrl: "https://mega-web.example/ok.rar", fileSize: null, retriesUsed: 0 };
});
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
const result = await service.unrestrictLink("https://rapidgator.net/file/per-account-creds");
// Jeder Account wurde mit SEINEM eigenen Login angesprochen (nicht 2x user1).
expect(accountsSeen).toContain("user1");
expect(accountsSeen).toContain("user2");
// Und der funktionierende Account 2 loest auf.
expect((result as { sourceAccountId?: string }).sourceAccountId).toBe(getMegaDebridAccountId("user2"));
expect(result.directUrl).toBe("https://mega-web.example/ok.rar");
}, 20000);
it("respects provider selection and does not append hidden providers", async () => {
const settings = {
...defaultSettings(),
+32
View File
@@ -89,6 +89,38 @@ describe("mega-web-fallback", () => {
expect(ajaxCalls).toBe(1);
});
it("logs in with the per-account credentials passed to unrestrict, not the default", async () => {
const loginsUsed: string[] = [];
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: { body?: unknown }) => {
const urlStr = String(url);
if (urlStr.includes("form=login")) {
const params = new URLSearchParams(String(opts?.body ?? ""));
loginsUsed.push(params.get("login") || "");
const headers = new Headers();
headers.append("set-cookie", "session=goodcookie; path=/");
return new Response("", { headers, status: 200 });
}
if (urlStr.includes("page=debrideur")) {
return new Response('<form id="debridForm"></form>', { status: 200 });
}
if (urlStr.includes("form=debrid")) {
return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l1</h3><a href="javascript:processDebrid(1,'code1',0)">d</a></div>`, { status: 200 });
}
if (urlStr.includes("ajax=debrid")) {
return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 });
}
return new Response("Not found", { status: 404 });
}) as unknown as typeof fetch;
// getCredentials liefert den DEFAULT/Legacy-Account ...
const fallback = new MegaWebFallback(() => ({ login: "defaultacc", password: "defpw" }));
// ... aber die Rotation übergibt explizit Account 2 — DESSEN Login MUSS verwendet werden.
const result = await fallback.unrestrict("https://mega.debrid/l1", undefined, { login: "account2", password: "pw2" });
expect(result?.directUrl).toBe("https://mega.direct/ok");
expect(loginsUsed).toContain("account2");
expect(loginsUsed).not.toContain("defaultacc");
});
it("throws if login fails to set cookie", async () => {
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
const urlStr = String(url);