real-debrid-downloader/src/main/mega-public-api.ts
Sucukdeluxe 3ed3877ac9 chore: remove all source code comments and internal artifacts
Strip every comment from the source (parsed with the TypeScript compiler so
strings, template literals, regex literals and JSX are never touched), and drop
internal/working artifacts that do not belong in the public repository
(design mockups, internal analysis docs, a stray backup file and an old log).
No functional change: build is green, the full test suite passes.
2026-06-06 04:53:54 +02:00

130 lines
3.9 KiB
TypeScript

import crypto from "node:crypto";
const MEGA_API_BASE = "https://g.api.mega.co.nz/cs";
const MEGA_API_TIMEOUT_MS = 12_000;
export interface MegaFileInfo {
name: string;
size: number;
}
const NEW_FORMAT_RE = /^https?:\/\/mega\.(?:nz|co\.nz)\/file\/([A-Za-z0-9_-]+)#([A-Za-z0-9_-]+)/i;
const LEGACY_FORMAT_RE = /^https?:\/\/mega\.(?:nz|co\.nz)\/#!([A-Za-z0-9_-]+)!([A-Za-z0-9_-]+)/i;
export function isMegaFileUrl(url: string): boolean {
const s = String(url || "").trim();
return NEW_FORMAT_RE.test(s) || LEGACY_FORMAT_RE.test(s);
}
function base64UrlDecode(s: string): Buffer | null {
let b64 = String(s || "").trim().replace(/-/g, "+").replace(/_/g, "/");
while (b64.length % 4 !== 0) b64 += "=";
try {
return Buffer.from(b64, "base64");
} catch {
return null;
}
}
export interface ParsedMegaLink {
id: string;
rawKey: Buffer;
}
export function parseMegaUrl(url: string): ParsedMegaLink | null {
const s = String(url || "").trim();
const m = NEW_FORMAT_RE.exec(s) || LEGACY_FORMAT_RE.exec(s);
if (!m) return null;
const id = m[1];
const rawKey = base64UrlDecode(m[2]);
if (!rawKey || rawKey.length !== 32) return null;
return { id, rawKey };
}
export function decryptMegaAttributes(encrypted: Buffer, aesKey: Buffer): Record<string, unknown> | null {
if (!Buffer.isBuffer(encrypted) || encrypted.length === 0 || encrypted.length % 16 !== 0) return null;
if (!Buffer.isBuffer(aesKey) || aesKey.length !== 16) return null;
let plain: Buffer;
try {
const decipher = crypto.createDecipheriv("aes-128-cbc", aesKey, Buffer.alloc(16));
decipher.setAutoPadding(false);
plain = Buffer.concat([decipher.update(encrypted), decipher.final()]);
} catch {
return null;
}
const text = plain.toString("utf8").replace(/\0+$/, "").trim();
if (!text.startsWith("MEGA{")) return null;
try {
return JSON.parse(text.slice(4));
} catch {
return null;
}
}
function withTimeoutSignal(parent: AbortSignal | undefined, timeoutMs: number): AbortSignal {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort("mega-api-timeout"), timeoutMs);
if (parent) {
if (parent.aborted) {
controller.abort(parent.reason);
} else {
parent.addEventListener("abort", () => controller.abort(parent.reason), { once: true });
}
}
controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true });
return controller.signal;
}
export async function resolveMegaFilename(
url: string,
signal?: AbortSignal
): Promise<MegaFileInfo | null> {
const parsed = parseMegaUrl(url);
if (!parsed) return null;
const aesKey = parsed.rawKey.subarray(0, 16);
const apiUrl = `${MEGA_API_BASE}?id=${Math.floor(Math.random() * 1e9)}`;
const body = JSON.stringify([{ a: "g", g: 1, p: parsed.id }]);
let response: Response;
try {
response = await fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
signal: withTimeoutSignal(signal, MEGA_API_TIMEOUT_MS)
});
} catch {
return null;
}
if (!response.ok) return null;
let payload: unknown;
try {
payload = await response.json();
} catch {
return null;
}
if (typeof payload === "number") return null;
if (!Array.isArray(payload) || payload.length === 0) return null;
const first = payload[0];
if (typeof first === "number") return null;
if (!first || typeof first !== "object") return null;
const info = first as { s?: unknown; at?: unknown; e?: unknown };
if (typeof info.e === "number" && info.e !== 0) return null;
const size = typeof info.s === "number" && info.s > 0 ? info.s : 0;
if (typeof info.at !== "string" || !info.at.trim()) return null;
const encryptedAttrs = base64UrlDecode(info.at);
if (!encryptedAttrs) return null;
const attrs = decryptMegaAttributes(encryptedAttrs, aesKey);
if (!attrs || typeof attrs.n !== "string" || !attrs.n.trim()) return null;
return { name: attrs.n.trim(), size };
}