get bearer for webuntis
This commit is contained in:
parent
c96916aa8e
commit
1163b61374
457
files/scripts/webuntis/getbearer.js
Normal file
457
files/scripts/webuntis/getbearer.js
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
// getbearer.js — Browserless /function (Puppeteer)
|
||||||
|
// output = "png" | "json" (default: "json")
|
||||||
|
// - "png": nach Schulwahl + Loginversuch Screenshot als image/png
|
||||||
|
// - "json": bearer/diag/seenAuth + screenshot_b64
|
||||||
|
|
||||||
|
export default async function ({ page, context }) {
|
||||||
|
const {
|
||||||
|
url = "https://webuntis.com/#/basic/login",
|
||||||
|
username = "LehmanLuc",
|
||||||
|
password = "",
|
||||||
|
school = "LMG Crailsheim", // ggf. "Lise-Meitner-Gymnasium"
|
||||||
|
debug = true,
|
||||||
|
timeoutMs = 60000,
|
||||||
|
maxTimeout = 15000,
|
||||||
|
screenshotFullPage = true,
|
||||||
|
output = "json" // "png" | "json"
|
||||||
|
} = context || {};
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||||
|
const diag = { steps: [], schoolHint: null, frameUrls: [], errors: [] };
|
||||||
|
const seenAuth = [];
|
||||||
|
let bearer = null;
|
||||||
|
|
||||||
|
// ---------- Hardening ----------
|
||||||
|
try { await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"); } catch {}
|
||||||
|
try { await page.setViewport({ width: 1366, height: 900, deviceScaleFactor: 1 }); } catch {}
|
||||||
|
try { page.setDefaultTimeout(Math.max(maxTimeout, timeoutMs)); } catch {}
|
||||||
|
|
||||||
|
// ---------- Network taps ----------
|
||||||
|
page.on("request", (req) => {
|
||||||
|
try {
|
||||||
|
const a = req.headers()["authorization"];
|
||||||
|
if (!bearer && a?.startsWith("Bearer ")) {
|
||||||
|
bearer = a.slice(7);
|
||||||
|
seenAuth.push({ t: "req", url: req.url() });
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
page.on("response", async (res) => {
|
||||||
|
try {
|
||||||
|
if (bearer) return;
|
||||||
|
const a = res.request().headers()["authorization"];
|
||||||
|
if (a?.startsWith("Bearer ")) {
|
||||||
|
bearer = a.slice(7);
|
||||||
|
seenAuth.push({ t: "res-reqhdr", url: res.url() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ct = (res.headers()["content-type"] || "").toLowerCase();
|
||||||
|
if (ct.includes("application/json")) {
|
||||||
|
const data = await res.json().catch(() => null);
|
||||||
|
const tok = data?.access_token || data?.token || data?.idToken || data?.id_token;
|
||||||
|
if (tok) { bearer = tok; seenAuth.push({ t: "res-json", url: res.url() }); }
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- Helpers ----------
|
||||||
|
async function queryDeepHandle(frame, predicateFn) {
|
||||||
|
const handle = await frame.evaluateHandle((predSrc) => {
|
||||||
|
const pred = eval(predSrc);
|
||||||
|
const visit = (root) => {
|
||||||
|
for (const el of root.querySelectorAll("*")) {
|
||||||
|
try { if (pred(el)) return el; } catch {}
|
||||||
|
if (el.shadowRoot) { const f = visit(el.shadowRoot); if (f) return f; }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return visit(document);
|
||||||
|
}, predicateFn.toString());
|
||||||
|
return handle.asElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function takePngBuffer() {
|
||||||
|
try { return await page.screenshot({ type: "png", fullPage: screenshotFullPage }); }
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeInto = async (el, val) => {
|
||||||
|
await el.focus().catch(() => {});
|
||||||
|
await el.click({ clickCount: 3 }).catch(() => {});
|
||||||
|
await el.type(val, { delay: 15 }).catch(() => {});
|
||||||
|
try {
|
||||||
|
await el.evaluate((e,v)=>{
|
||||||
|
const last = e.value;
|
||||||
|
e.value = v;
|
||||||
|
e.dispatchEvent(new Event('input', {bubbles:true}));
|
||||||
|
if (last !== v) e.dispatchEvent(new Event('change', {bubbles:true}));
|
||||||
|
if (e.form) e.form.dispatchEvent(new Event('input', {bubbles:true}));
|
||||||
|
}, val);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aktion + Navigation gekoppelt ausführen (verhindert "execution context destroyed")
|
||||||
|
async function withNav(page, action, waitMs = maxTimeout) {
|
||||||
|
const navP = page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: waitMs }).catch(() => {});
|
||||||
|
const hostP = page.waitForFunction(() => {
|
||||||
|
try {
|
||||||
|
const h = location.hostname;
|
||||||
|
return /^[^.]+\.(webuntis|untis)\.com$/i.test(h) && !/^(www\.)?webuntis\.com$/i.test(h);
|
||||||
|
} catch { return false; }
|
||||||
|
}, { timeout: waitMs }).catch(() => {});
|
||||||
|
const actP = (async () => { try { await action(); } catch {} })();
|
||||||
|
await Promise.race([navP, hostP]);
|
||||||
|
await Promise.allSettled([navP, hostP, actP]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForRedirectOrPwd(page, timeout = 20000) {
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < timeout) {
|
||||||
|
try {
|
||||||
|
const host = new URL(page.url()).hostname;
|
||||||
|
if (/^[^.]+\.(webuntis|untis)\.com$/i.test(host) && !/^(www\.)?webuntis\.com$/i.test(host)) return true;
|
||||||
|
} catch {}
|
||||||
|
for (const fr of page.frames()) {
|
||||||
|
const pwd = await queryDeepHandle(fr, (el) => el.tagName === "INPUT" && (el.type?.toLowerCase?.() === "password"));
|
||||||
|
if (pwd) return true;
|
||||||
|
}
|
||||||
|
await sleep(300);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Robuste Schulwahl
|
||||||
|
async function pickSchool(page, schoolHint, diag) {
|
||||||
|
if (!schoolHint) { diag.steps.push("no-school-hint"); return false; }
|
||||||
|
|
||||||
|
// 1) Suchfeld finden (shadowDOM tauglich)
|
||||||
|
let input = null;
|
||||||
|
for (const fr of page.frames()) {
|
||||||
|
const h = await queryDeepHandle(fr, (el) => {
|
||||||
|
if (el.tagName !== "INPUT") return false;
|
||||||
|
const type = (el.getAttribute("type") || "").toLowerCase();
|
||||||
|
const role = (el.getAttribute("role") || "").toLowerCase();
|
||||||
|
const ph = (el.getAttribute("placeholder") || "").toLowerCase();
|
||||||
|
const ar = (el.getAttribute("aria-label") || "").toLowerCase();
|
||||||
|
return type === "search" || role === "combobox" || /search|schule|school/.test(ph+ar);
|
||||||
|
});
|
||||||
|
if (h) { input = h; break; }
|
||||||
|
}
|
||||||
|
if (!input) { diag.steps.push("school-input-not-found"); return false; }
|
||||||
|
|
||||||
|
// 2) Tippen
|
||||||
|
diag.steps.push("school-input-found");
|
||||||
|
try { await input.focus(); } catch {}
|
||||||
|
try { await input.click({ clickCount: 3 }); } catch {}
|
||||||
|
await page.keyboard.type(String(schoolHint), { delay: 25 });
|
||||||
|
await sleep(700);
|
||||||
|
|
||||||
|
// 3) Erste sichtbare Suggestion unter dem Input per BBox ermitteln
|
||||||
|
async function getBox(elHandle) { try { return await elHandle.boundingBox(); } catch { return null; } }
|
||||||
|
async function isVisible(el) {
|
||||||
|
try {
|
||||||
|
return await el.evaluate((e) => {
|
||||||
|
const s = window.getComputedStyle(e);
|
||||||
|
const r = e.getBoundingClientRect();
|
||||||
|
return s && s.display !== "none" && s.visibility !== "hidden" && r.width > 1 && r.height > 1;
|
||||||
|
});
|
||||||
|
} catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputBox = await getBox(input);
|
||||||
|
let best = null; // { frame, handle, box, text, key }
|
||||||
|
const until = Date.now() + 6000;
|
||||||
|
|
||||||
|
while (!best && Date.now() < until) {
|
||||||
|
for (const fr of page.frames()) {
|
||||||
|
let handles = [];
|
||||||
|
for (const sel of ['[role="option"]', '[role="listitem"]', 'li', 'div[role="option"]']) {
|
||||||
|
try { const arr = await fr.$$(sel); if (arr?.length) handles.push(...arr); } catch {}
|
||||||
|
}
|
||||||
|
for (const h of handles) {
|
||||||
|
try {
|
||||||
|
if (!(await isVisible(h))) { await h.dispose().catch(()=>{}); continue; }
|
||||||
|
const box = await getBox(h);
|
||||||
|
if (!box || !inputBox) { await h.dispose().catch(()=>{}); continue; }
|
||||||
|
if (box.y < inputBox.y + inputBox.height - 2) { await h.dispose().catch(()=>{}); continue; }
|
||||||
|
const dist = Math.abs(box.y - (inputBox.y + inputBox.height));
|
||||||
|
const text = (await h.evaluate(e => (e.textContent || "").trim())).toLowerCase();
|
||||||
|
const hint = String(schoolHint).toLowerCase();
|
||||||
|
const aliases = ["lise", "meitner", "lise-meitner", "gymnasium", "lmg", "crailsheim"];
|
||||||
|
const score = (text.includes(hint) || aliases.some(a => text.includes(a))) ? 0 : 1;
|
||||||
|
const key = score * 100000 + dist;
|
||||||
|
if (!best || key < best.key) best = { frame: fr, handle: h, box, text, key };
|
||||||
|
else await h.dispose().catch(()=>{});
|
||||||
|
} catch { try { await h.dispose(); } catch {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!best) await sleep(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
let clicked = false;
|
||||||
|
if (best?.handle) {
|
||||||
|
try {
|
||||||
|
const { x, y, width, height } = best.box;
|
||||||
|
await withNav(page, async () => {
|
||||||
|
await page.mouse.move(Math.round(x + width / 2), Math.round(y + height / 2));
|
||||||
|
await page.mouse.click(Math.round(x + width / 2), Math.round(y + height / 2));
|
||||||
|
}, maxTimeout);
|
||||||
|
clicked = true;
|
||||||
|
diag.steps.push("school-suggestion-clicked-bbox:" + (best.text || ""));
|
||||||
|
} catch (e) {
|
||||||
|
diag.errors.push("bbox-click-failed:" + (e?.message || e));
|
||||||
|
try {
|
||||||
|
await withNav(page, async () => { await best.handle.click(); }, maxTimeout);
|
||||||
|
clicked = true;
|
||||||
|
diag.steps.push("school-suggestion-clicked-handle");
|
||||||
|
} catch {}
|
||||||
|
} finally {
|
||||||
|
try { await best.handle.dispose(); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clicked) {
|
||||||
|
// Sicherstellen, dass das Feld Fokus hat; dann ArrowDown+Enter + Nav abwarten
|
||||||
|
try { await input.focus(); } catch {}
|
||||||
|
await withNav(page, async () => {
|
||||||
|
await page.keyboard.press("ArrowDown");
|
||||||
|
await sleep(120);
|
||||||
|
await page.keyboard.press("Enter");
|
||||||
|
}, maxTimeout);
|
||||||
|
diag.steps.push("school-enter-fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(500); // nach Navigation alte Handles verwerfen
|
||||||
|
return await waitForRedirectOrPwd(page, 20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Login-Felder finden & ausfüllen (robust) ----
|
||||||
|
// ---- Login-Felder finden & ausfüllen (robust, ohne :has-text) ----
|
||||||
|
async function findLoginElements() {
|
||||||
|
// 1) direkte, einfache Selektoren
|
||||||
|
const userSel = [
|
||||||
|
'input[placeholder*="Benutzer" i]',
|
||||||
|
'input[aria-label*="Benutzer" i]',
|
||||||
|
'input[name*="user" i]',
|
||||||
|
'input[id*="user" i]',
|
||||||
|
'input[type="email"]',
|
||||||
|
'input[type="text"]'
|
||||||
|
].join(',');
|
||||||
|
|
||||||
|
const passSel = [
|
||||||
|
'input[placeholder*="Passwort" i]',
|
||||||
|
'input[aria-label*="Passwort" i]',
|
||||||
|
'input[type="password"]'
|
||||||
|
].join(',');
|
||||||
|
|
||||||
|
let u = await page.$(userSel);
|
||||||
|
let p = await page.$(passSel);
|
||||||
|
|
||||||
|
// 2) Shadow-DOM-Fallback
|
||||||
|
if (!u || !p) {
|
||||||
|
for (const fr of page.frames()) {
|
||||||
|
if (!u) u = await queryDeepHandle(fr, (el) => {
|
||||||
|
if (el.tagName !== "INPUT") return false;
|
||||||
|
const t = (el.getAttribute("type")||"").toLowerCase();
|
||||||
|
const txt = ((el.getAttribute("placeholder")||"") + " " + (el.getAttribute("aria-label")||"")).toLowerCase();
|
||||||
|
return (t==="text"||t==="email"||t==="") && /user|benutzer|name/.test(txt);
|
||||||
|
});
|
||||||
|
if (!p) p = await queryDeepHandle(fr, (el) => el.tagName==="INPUT" && (el.type||"").toLowerCase()==="password");
|
||||||
|
if (u && p) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Submit-Button: zuerst type=submit, sonst per Textinhalt
|
||||||
|
let b = await page.$('button[type="submit"], input[type="submit"]');
|
||||||
|
|
||||||
|
if (!b) {
|
||||||
|
// scannt sichtbare Buttons und matcht Textinhalt
|
||||||
|
const btnByText = async (root) => {
|
||||||
|
const LABELS = ["login","anmelden","einloggen","weiter","sign in","absenden"];
|
||||||
|
const nodes = root.querySelectorAll('button, input[type="submit"]');
|
||||||
|
for (const n of nodes) {
|
||||||
|
let txt = (n.innerText || n.textContent || "").toLowerCase().trim();
|
||||||
|
if (!txt && n.getAttribute) txt = (n.getAttribute("value")||"").toLowerCase().trim();
|
||||||
|
if (!txt) continue;
|
||||||
|
if (LABELS.some(l=>txt.includes(l))) {
|
||||||
|
const cs = getComputedStyle(n);
|
||||||
|
if (cs.display!=="none" && cs.visibility!=="hidden" && !n.disabled) return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// zuerst im Haupt-DOM
|
||||||
|
b = await page.evaluateHandle(btnByText, document).then(h=>h.asElement()).catch(()=>null);
|
||||||
|
|
||||||
|
// Shadow-DOM-Fallback
|
||||||
|
if (!b) {
|
||||||
|
for (const fr of page.frames()) {
|
||||||
|
const h = await fr.evaluateHandle(() => {
|
||||||
|
const visit = (root) => {
|
||||||
|
const el = (function(root){
|
||||||
|
const LABELS = ["login","anmelden","einloggen","weiter","sign in","absenden"];
|
||||||
|
const nodes = root.querySelectorAll('button, input[type="submit"]');
|
||||||
|
for (const n of nodes) {
|
||||||
|
let txt = (n.innerText || n.textContent || "").toLowerCase().trim();
|
||||||
|
if (!txt && n.getAttribute) txt = (n.getAttribute("value")||"").toLowerCase().trim();
|
||||||
|
if (!txt) continue;
|
||||||
|
if (LABELS.some(l=>txt.includes(l))) {
|
||||||
|
const cs = getComputedStyle(n);
|
||||||
|
if (cs.display!=="none" && cs.visibility!=="hidden" && !n.disabled) return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})(root);
|
||||||
|
if (el) return el;
|
||||||
|
for (const e of root.querySelectorAll("*")) {
|
||||||
|
if (e.shadowRoot) {
|
||||||
|
const f = visit(e.shadowRoot);
|
||||||
|
if (f) return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return visit(document);
|
||||||
|
}).catch(()=>null);
|
||||||
|
if (h) { b = h.asElement(); break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Wenn noch kein Button: im selben <form> wie Passwortfeld
|
||||||
|
if (!b && p) {
|
||||||
|
const form = await p.evaluateHandle(e => e.closest('form'));
|
||||||
|
if (form) {
|
||||||
|
const hb = await form.asElement().$('button, input[type="submit"]');
|
||||||
|
if (hb) b = hb;
|
||||||
|
await form.dispose().catch(()=>{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { u, p, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------- Main flow ----------
|
||||||
|
try {
|
||||||
|
// Load (Blank-Heuristik)
|
||||||
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs }).catch(() => {});
|
||||||
|
await sleep(700);
|
||||||
|
try {
|
||||||
|
const isBlank = await page.evaluate(() => document.body && document.body.children.length < 2);
|
||||||
|
if (isBlank) { diag.steps.push("blank-reload"); await page.reload({ waitUntil: "domcontentloaded" }).catch(()=>{}); await sleep(500); }
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Schul-Hint bestimmen
|
||||||
|
let schoolHint = school || null;
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
const m = u.hostname.match(/^([^.]+)\.webuntis\.com$/i);
|
||||||
|
if (!schoolHint && m) schoolHint = m[1];
|
||||||
|
} catch {}
|
||||||
|
diag.schoolHint = schoolHint || null;
|
||||||
|
|
||||||
|
// Consent (best effort)
|
||||||
|
for (const sel of [
|
||||||
|
'button[aria-label*="Akzept"]','button[title*="Akzept"]',
|
||||||
|
'button[aria-label*="Accept" i]','button[title*="Accept" i]'
|
||||||
|
]) { try { await page.click(sel, { timeout: 500 }); diag.steps.push("clicked-consent"); break; } catch {} }
|
||||||
|
|
||||||
|
// Passwortfeld schon sichtbar?
|
||||||
|
let hasPwd = false;
|
||||||
|
for (const fr of page.frames()) {
|
||||||
|
const pwd = await queryDeepHandle(fr, (el) => el.tagName === "INPUT" && (el.type?.toLowerCase?.() === "password"));
|
||||||
|
if (pwd) { hasPwd = true; break; }
|
||||||
|
}
|
||||||
|
if (!hasPwd) {
|
||||||
|
const ok = await pickSchool(page, schoolHint, diag);
|
||||||
|
if (!ok) diag.steps.push("school-pick-maybe-failed");
|
||||||
|
await sleep(600);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login-Felder und Submit
|
||||||
|
let { u: userEl, p: passEl, b: btnEl } = await findLoginElements();
|
||||||
|
if (!userEl || !passEl) {
|
||||||
|
diag.steps.push("login-fields-missing");
|
||||||
|
} else {
|
||||||
|
await typeInto(userEl, username);
|
||||||
|
await typeInto(passEl, password);
|
||||||
|
|
||||||
|
const submitAction = async () => {
|
||||||
|
if (btnEl) {
|
||||||
|
try { await btnEl.click(); }
|
||||||
|
catch { try { await passEl.press("Enter"); } catch {} }
|
||||||
|
} else {
|
||||||
|
try { await passEl.press("Enter"); } catch {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navP = page.waitForNavigation({ waitUntil: "networkidle0", timeout: maxTimeout }).catch(()=>{});
|
||||||
|
const bearerP = new Promise(resolve=>{
|
||||||
|
const t = setTimeout(()=>resolve(false), maxTimeout);
|
||||||
|
const off = page.on("request", req=>{
|
||||||
|
const a = req.headers()?.authorization||"";
|
||||||
|
if (a.startsWith("Bearer ")) { clearTimeout(t); resolve(true); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const errBannerP = page.waitForSelector('[role="alert"], .error, .un-input-group__error', { timeout: 12000 })
|
||||||
|
.then(()=>true).catch(()=>false);
|
||||||
|
|
||||||
|
await submitAction();
|
||||||
|
const results = await Promise.allSettled([navP, bearerP, errBannerP]);
|
||||||
|
const hadErr = results[2]?.value === true;
|
||||||
|
if (hadErr) diag.steps.push("login-error-banner-seen");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage-Fallback
|
||||||
|
if (!bearer) {
|
||||||
|
try {
|
||||||
|
for (const fr of page.frames()) {
|
||||||
|
const tok = await fr.evaluate(() => {
|
||||||
|
const scan = (s) => {
|
||||||
|
for (let i=0;i<s.length;i++){
|
||||||
|
const v = s.getItem(s.key(i));
|
||||||
|
if (!v) continue;
|
||||||
|
try {
|
||||||
|
const j = JSON.parse(v);
|
||||||
|
const t = j?.access_token || j?.token || j?.idToken || j?.id_token;
|
||||||
|
if (t) return t;
|
||||||
|
} catch {}
|
||||||
|
if (/^Bearer\s+/.test(v)) return v.replace(/^Bearer\s+/, "");
|
||||||
|
if (/eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\./.test(v)) return v;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return scan(localStorage) || scan(sessionStorage);
|
||||||
|
}).catch(()=>null);
|
||||||
|
if (tok) { bearer = tok; diag.steps.push("token-from-storage"); break; }
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- OUTPUT ----------
|
||||||
|
if (output === "png") {
|
||||||
|
const buf = await takePngBuffer();
|
||||||
|
return { data: buf || Buffer.from([]), type: "image/png" };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const buf = await takePngBuffer();
|
||||||
|
const screenshot_b64 = buf ? "data:image/png;base64," + buf.toString("base64") : null;
|
||||||
|
|
||||||
|
const json = debug
|
||||||
|
? { bearer: bearer || null, seenAuth, diag, screenshot_b64 }
|
||||||
|
: { bearer: bearer || null, screenshot_b64 };
|
||||||
|
return { data: json, type: "application/json" };
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
diag.errors.push(err?.message || String(err));
|
||||||
|
const buf = await takePngBuffer();
|
||||||
|
if (output === "png") return { data: buf || Buffer.from([]), type: "image/png" };
|
||||||
|
const screenshot_b64 = buf ? "data:image/png;base64," + buf.toString("base64") : null;
|
||||||
|
return { data: { error: err?.message || String(err), diag, seenAuth, bearer: null, screenshot_b64 }, type: "application/json" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user