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 | 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 { 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 }; }