#!/usr/bin/env node // Puppeteer script to open a page, click on a button with text 'Start meeting' (or similar), // capture network requests (XHR/fetch), detect wss/ws urls, save a screenshot and print a JSON summary. import puppeteer from 'puppeteer'; import fs from 'fs'; async function findButtonByText(page, text) { const xpath = `//button[contains(translate(normalize-space(.), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '${text.toLowerCase()}') or contains(translate(@value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '${text.toLowerCase()}') or contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '${text.toLowerCase()}')]`; const els = await page.$x(xpath); return els[0] || null; } async function findInputSelector(page) { const selectors = [ 'input[name="name"]', 'input[name="username"]', 'input[placeholder*="Name"]', 'input[placeholder*="name"]', 'input[placeholder*="Usuario"]', 'input[type="text"]', 'input#name', 'input#username', 'input[class*=name]', 'input[class*=user]' ]; for (const s of selectors) { try { const el = await page.$(s); if (el) return { selector: s, element: el }; } catch (e) {} } // try to find input inside modal/dialogs const inputs = await page.$$('input'); for (const inp of inputs) { try { const visible = await inp.boundingBox(); if (visible) return { selector: null, element: inp }; } catch (e) {} } return null; } async function clickButtonByTextVariants(page, variants) { for (const v of variants) { const btn = await findButtonByText(page, v); if (btn) { try { await btn.click({ delay: 50 }); return { found: true, text: v }; } catch(e) { return { found: false, error: String(e) }; } } } return { found: false }; } async function main() { const url = process.argv[2] || 'http://192.168.1.19:3000'; const outPrefix = process.argv[3] || 'start_meeting_capture'; const headless = process.env.PW_HEADLESS !== '0'; const browser = await puppeteer.launch({ headless, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); const requests = []; const responses = []; const websocketUrls = new Set(); // Prepare an initial empty summary so we can return it even on early failure let summary = { requestedUrl: url, finalUrl: null, screenshot: null, requestsCount: 0, responsesCount: 0, websocketUrls: [], requests: [], responses: [], console: [], pageErrors: [], foundToken: null, foundSessionId: null, validate: { checked: false } }; page.on('request', (req) => { const r = { id: req._requestId || null, url: req.url(), method: req.method(), resourceType: req.resourceType(), headers: req.headers() }; // try to capture postData if available try { r.postData = req.postData ? req.postData() : null; } catch(e) {} requests.push(r); }); page.on('response', async (resp) => { try { const req = resp.request(); const entry = { url: resp.url(), status: resp.status(), statusText: resp.statusText(), headers: resp.headers(), request: { url: req.url(), method: req.method(), headers: req.headers() } }; // If response is small JSON/text include preview let text = null; try { const ct = (resp.headers()['content-type'] || resp.headers()['Content-Type'] || '') + ''; if (ct.includes('application/json') || ct.includes('text/')) { text = await resp.text(); if (text && text.length > 2048) text = text.slice(0, 2048) + '...'; } } catch (e) {} if (text) entry.bodyPreview = text; responses.push(entry); if (resp.status() === 101) websocketUrls.add(resp.url()); } catch (e) { // ignore } }); // also capture console messages const consoleMessages = []; page.on('console', (msg) => { try { const args = msg.args ? msg.args.map(a => { try { return a.jsonValue(); } catch (e) { return String(a); } }) : []; consoleMessages.push({ type: msg.type(), text: msg.text(), args }); } catch (e) {} }); // catch page errors const pageErrors = []; page.on('pageerror', err => pageErrors.push(String(err))); let finalUrl = url; let navigationFailed = false; try { await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }); finalUrl = page.url(); } catch (e) { // handle connection refused or other navigation errors gracefully console.error('Failed to load page:', String(e)); navigationFailed = true; } // update summary basics summary.requestedUrl = url; summary.finalUrl = finalUrl; // try to find start button const possibleTexts = ['start meeting','start','join meeting','start session','start meeting now','iniciar reunión','iniciar','comenzar','abrir estudio']; let btn = null; if (!navigationFailed) { for (const t of possibleTexts) { btn = await findButtonByText(page, t); if (btn) { console.log('Found button with text:', t); break; } } if (!btn) { // also try common selectors const selectors = ['button.start','button.join','button#start','button#join','button[data-action="start"]']; for (const s of selectors) { try { const el = await page.$(s); if (el) { btn = el; console.log('Found button by selector:', s); break; } } catch(e){} } } } else { console.warn('Skipping button search because navigation failed'); } if (btn) { try { await btn.click({ delay: 50 }); console.log('Clicked start button, waiting for network activity...'); try { await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 8000 }); finalUrl = page.url(); } catch (e) {} await page.waitForTimeout(800); // After clicking Start, try to find input for name and Join button console.log('Looking for name input...'); const inputInfo = await findInputSelector(page); if (inputInfo && inputInfo.element) { try { console.log('Found input selector:', inputInfo.selector || ''); await inputInfo.element.focus(); await page.keyboard.type('Xesar', { delay: 80 }); await page.waitForTimeout(300); } catch(e){ console.warn('Failed to type into input', String(e)); } } else { console.log('No obvious input found; trying to find input with dialog heuristics...'); } // Click Join button (variants) const joinVariants = ['join room','join','join meeting','join now','join room','unirse','unirse a la sala','entrar','entrar al estudio','join session','join room']; const joinRes = await clickButtonByTextVariants(page, joinVariants.map(v => v + '')); if (joinRes && joinRes.found) { console.log('Clicked join button, variant:', joinRes.text); try { await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 8000 }); finalUrl = page.url(); } catch(e) {} await page.waitForTimeout(1200); } else { console.log('Join button not found by text variants, trying selectors...'); const joinSelectors = ['button.join','button#join','button[data-action="join"]','button[data-action="enter"]','button.btn-primary']; for (const s of joinSelectors) { try { const el = await page.$(s); if (el) { await el.click({ delay: 50 }); console.log('Clicked join selector', s); try { await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 8000 }); finalUrl = page.url(); } catch(e){} break; } } catch(e){} } } } catch (e) { console.error('Failed to click start/join flow:', String(e)); } } else { console.warn('Start button not found on page.'); } // snapshot const screenshotPath = `/tmp/${outPrefix}.png`; try { await page.screenshot({ path: screenshotPath, fullPage: true }); summary.screenshot = screenshotPath; } catch (e) { summary.screenshot = null; } // try to detect websocket connections from requests list for (const r of requests) { if (r.url.startsWith('ws://') || r.url.startsWith('wss://')) websocketUrls.add(r.url); } // Attempt to extract token or session id from responses or requests let foundToken = null; let foundSessionId = null; try { // search responses for token or id for (const resp of responses) { try { const body = resp.bodyPreview; if (!body) continue; // try JSON let parsed = null; try { parsed = JSON.parse(body); } catch(e) { /* not json */ } if (parsed) { // common shapes: { token }, { token: '...', url: '...', id: '...' }, { id, studioUrl } if (!foundToken && (parsed.token || parsed.access_token || parsed.data?.token)) { foundToken = parsed.token || parsed.access_token || parsed.data?.token; } if (!foundSessionId && (parsed.id || parsed.sessionId || parsed.data?.id)) { foundSessionId = parsed.id || parsed.sessionId || parsed.data?.id; } // sometimes token nested for (const k of Object.keys(parsed)) { if (!foundToken && typeof parsed[k] === 'string' && parsed[k].startsWith('eyJ')) { foundToken = parsed[k]; } } } else { // search raw text for token-like pattern const m = body.match(/"token"\s*:\s*"([A-Za-z0-9-_\.]+)"/); if (m) foundToken = m[1]; const m2 = body.match(/"id"\s*:\s*"([A-Za-z0-9-_]+)"/); if (m2 && !foundSessionId) foundSessionId = m2[1]; } } catch(e) {} if (foundToken && foundSessionId) break; } // search requests postData if (!foundToken || !foundSessionId) { for (const req of requests) { try { const pd = req.postData; if (!pd) continue; let parsed = null; try { parsed = JSON.parse(pd); } catch(e) {} if (parsed) { if (!foundToken && (parsed.token || parsed.access_token)) foundToken = parsed.token || parsed.access_token; if (!foundSessionId && (parsed.id || parsed.sessionId || parsed.room)) { // sometimes they send room and server creates session id; keep room for debug foundSessionId = parsed.id || parsed.sessionId; } // also check nested for (const k of Object.keys(parsed)) { if (!foundToken && typeof parsed[k] === 'string' && parsed[k].startsWith('eyJ')) foundToken = parsed[k]; } } } catch(e) {} if (foundToken && foundSessionId) break; } } } catch(e) { // ignore } const validateResult = { checked: false }; try { const BACKEND = process.env.BACKEND || 'http://localhost:4000'; // If we found a session id but no token, try to fetch token via backend API if (!foundToken && foundSessionId) { try { const r = await fetch(`${BACKEND.replace(/\/$/, '')}/api/session/${encodeURIComponent(foundSessionId)}/token`); if (r.ok) { const js = await r.json(); if (js && js.token) foundToken = js.token; } } catch(e) { /* ignore */ } } // If we have a token, call validate proxy if (foundToken) { validateResult.checked = true; try { const r2 = await fetch(`${BACKEND.replace(/\/$/, '')}/api/session/validate?token=${encodeURIComponent(foundToken)}`); const ct = r2.headers.get('content-type') || ''; const text = await r2.text(); let body = null; try { body = JSON.parse(text); } catch(e) { body = text } validateResult.ok = r2.ok; validateResult.status = r2.status; validateResult.body = body; } catch (e) { validateResult.error = String(e); } // decode JWT header to inspect alg try { const parts = foundToken.split('.'); if (parts.length >= 2) { const header = parts[0]; const padded = header + '='.repeat((4 - (header.length % 4)) % 4); const decoded = Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'); try { validateResult.header = JSON.parse(decoded); } catch(e) { validateResult.header = decoded; } } } catch(e) { /* ignore */ } } } catch(e) { validateResult.error = String(e); } // After detection/validation, populate summary counts and arrays try { summary.requestsCount = requests.length; summary.responsesCount = responses.length; summary.websocketUrls = Array.from(websocketUrls); summary.requests = requests.slice(-100); summary.responses = responses.slice(-100); summary.console = consoleMessages.slice(-200); summary.pageErrors = pageErrors.slice(-50); } catch (e) { /* ignore */ } // attach foundToken/sessionId to summary for convenience summary.foundToken = foundToken || null; summary.foundSessionId = foundSessionId || null; summary.validate = validateResult; const outPath = `/tmp/${outPrefix}.json`; fs.writeFileSync(outPath, JSON.stringify(summary, null, 2)); console.log('Wrote summary to', outPath); console.log(JSON.stringify({ summaryPath: outPath, screenshot: summary.screenshot })); // Print the summary to stdout for environments where /tmp isn't accessible try { console.log(JSON.stringify({ summary }, null, 2)); } catch(e) { console.log('Failed to print summary to stdout', String(e)); } await browser.close(); } main().catch(err => { console.error('Error in script', err); process.exit(1); });