// Usage: node scripts/mirror.js [output-dir] // Renders the target page in headless Chromium with the cookie, waits for // networkidle, captures final DOM + every fetched resource, rewrites URLs to // point to the local mirror, strips `; function urlToSafePath(u) { const parsed = new URL(u); let p = parsed.pathname; if (parsed.search) { let safe = parsed.search.slice(1).replace(/[^a-zA-Z0-9._-]/g, '_'); // Hash overly long query strings to avoid ENAMETOOLONG if (safe.length > 80) { safe = crypto.createHash('md5').update(parsed.search).digest('hex').slice(0, 12); } p += '_q_' + safe; } // also hash if path itself is way too long const basename = path.basename(p); if (basename.length > 200) { const ext = path.extname(p); const dir = path.dirname(p); p = dir + '/' + crypto.createHash('md5').update(basename).digest('hex').slice(0, 16) + ext; } return p.replace(/^\/+/, ''); } async function main() { const [targetUrl, cookieValue, outDir] = process.argv.slice(2); if (!targetUrl || !cookieValue) { console.error('Usage: node mirror.js [out=/app/public/q]'); process.exit(1); } const OUT = outDir || '/app/public/q'; const MIRROR = path.join(OUT, 'mirror'); const urlObj = new URL(targetUrl); const ORIGIN = urlObj.origin; const HOST = urlObj.hostname; console.log(`[mirror] launching chromium → ${targetUrl}`); const browser = await chromium.launch({ headless: true }); const ctx = await browser.newContext({ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', viewport: { width: 1440, height: 900 }, }); await ctx.addCookies([{ name: 'MoodleSession', value: cookieValue, domain: HOST, path: '/', secure: true, sameSite: 'None', }]); const resources = new Map(); const page = await ctx.newPage(); page.on('response', async (resp) => { const rurl = resp.url(); if (!rurl.startsWith('http')) return; try { const buf = await resp.body(); if (buf && buf.length > 0) resources.set(rurl, buf); } catch (e) { /* redirect/204/etc */ } }); await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 90000 }); console.log(`[mirror] networkidle reached. extra settle…`); await page.waitForTimeout(4000); // Re-inline all CSS into