// Browser QA helpers: local credentials are read only; receipts omit cookies and auth bodies. const fs = require('node:fs'), path = require('node:path'), assert = require('node:assert/strict'), crypto = require('node:crypto'); const { chromium, } = require('C:/Users/macbook/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); const ROOT = path.resolve(__dirname, '..'), DIR = path.join(ROOT, 'storage/app/private/investmoz-v2/portal-audit'); const plan=JSON.parse(fs.readFileSync(path.join(DIR,'workflow-upgrade-final-latest.json'),'utf8')); const BASE=plan.base,TARGET=plan.target.database+':'+plan.target.port; const actors=JSON.parse(fs.readFileSync(path.join(DIR,'workflow-upgrade-final-actors-private.json'),'utf8')); function otp(secret) { let bits = 0, value = 0; const bytes = [], alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; for (const char of secret.toUpperCase().replace(/=+$/, '')) { const digit = alphabet.indexOf(char); assert(digit >= 0); value = (value << 5) | digit; bits += 5; if (bits >= 8) { bytes.push((value >>> (bits - 8)) & 255); bits -= 8; } } const counter = Buffer.alloc(8); counter.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 30000))); const digest = crypto .createHmac('sha1', Buffer.from(bytes)) .update(counter) .digest(), offset = digest[19] & 15; return String( (digest.readUInt32BE(offset) & 0x7fffffff) % 1000000, ).padStart(6, '0'); } function sanitized(error) { let message = String(error.message || error); for (const a of Object.values(actors)) for (const k of ['password', 'totp_secret', 'secret']) if (a[k]) message = message.replaceAll(a[k], '[REDACTED]'); return message .replace( /^.*(?:cookie:|x-xsrf-token:|authorization:).*$/gim, '[AUTH HEADER OMITTED]', ) .replace(/\b\d{6}\b/g, '[OTP OMITIDO]') .slice(0, 1800); } async function start(name) { const receipt = path.join(DIR, name + '.json'), stamp = new Date().toISOString().replace(/[:.]/g, '-'); const prior = fs.existsSync(receipt) ? JSON.parse(fs.readFileSync(receipt, 'utf8')) : {}; if (fs.existsSync(receipt)) fs.copyFileSync( receipt, path.join(DIR, name + '-attempt-' + stamp + '.json'), ); const proof = { started_at: new Date().toISOString(), base: BASE, target: TARGET, browser: 'Chromium 1243, isolated contexts', active_writes: 0, checks: {}, events: [], screenshots: [], }; const shots = path.join(DIR, name + '-' + stamp); fs.mkdirSync(shots, { recursive: true }); const browser = await chromium.launch({ headless: true, executablePath: 'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe', args: ['--host-resolver-rules=MAP portal-investidor.test 127.0.0.1', '--no-proxy-server'], }); const assets = new Map(); // Only immutable public QA build assets; never cache authenticated pages/API. function save() { fs.writeFileSync(receipt, JSON.stringify(proof, null, 2)); } function phase(value) { proof.phase = value; save(); console.log(JSON.stringify({ phase: value })); } async function context(role, width = 1440) { const ctx = await browser.newContext({ viewport: { width, height: 1000 }, locale: 'pt-MZ', ignoreHTTPSErrors: false, reducedMotion: 'reduce', }); ctx.qaConfirmed = false; await ctx.route('**/*', async (route) => { try { const req = route.request(), u = new URL(req.url()); if (u.origin !== BASE) return route.abort(); if (!['GET', 'HEAD'].includes(req.method())) assert( ctx.qaConfirmed, 'QA target must be confirmed before mutation', ); return await route.continue(); } catch (error) { if (ctx.qaClosing || page.isClosed()) return; (proof.harness_errors ||= []).push(sanitized(error)); save(); try { await route.abort(); } catch {} } }); ctx.on('response', (r) => { const u = new URL(r.url()); if (u.origin === BASE && !u.pathname.startsWith('/build-qa-')) { const h = r.headers(); proof.events.push({ actor: role, method: r.request().method(), path: u.pathname, status: r.status(), target: h['x-investmoz-qa-target'] || null, redirect: h.location ? new URL(h.location, BASE).pathname : null, }); save(); } }); const page = await ctx.newPage(); ctx.on('close', () => { ctx.qaClosing = true; }); page.setDefaultTimeout(25000); page.setDefaultNavigationTimeout(45000); page.on('pageerror', (e) => { (proof.javascript_errors ||= []).push(sanitized(e)); save(); }); return { ctx, page }; } async function open(page, url) { const r = await page.goto(BASE + url, { waitUntil: 'domcontentloaded', }); assert.equal(r.headers()['x-investmoz-qa-target'], TARGET); page.context().qaConfirmed = true; return r; } async function login(page, role) { const a = actors[role]; assert(a, 'Known local QA actor'); await open(page, '/login'); await page.locator('#email').fill(a.email); await page.locator('#password').fill(a.password); await page.locator('[data-test="login-button"]').click(); await page.waitForURL((u) => u.pathname !== '/login', {waitUntil:'domcontentloaded'}); if (new URL(page.url()).pathname === '/two-factor-challenge') { if (Date.now() % 30000 > 12000) await page.waitForTimeout(30500 - Date.now() % 30000); await page.locator('input[name="code"]').fill(otp(a.totp_secret)); await page .getByRole('button', { name: 'Continue', exact: true }) .click(); await page.waitForURL( (u) => !['/login', '/two-factor-challenge'].includes(u.pathname), {waitUntil:'domcontentloaded'}, ); } await page.locator('main').first().waitFor(); const response = await open(page, '/dashboard'); const html = await response.text(); const encoded = html.match(/data-page="([^"]+)"/); let body; if (encoded && encoded[1].startsWith('{')) { body = JSON.parse( encoded[1] .replace(/"/g, '"') .replace(/&/g, '&') .replace(/'/g, "'"), ); } else { const script = html.match( /]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/, ); assert( script, 'Inertia bootstrap must identify authenticated actor', ); body = JSON.parse(script[1]); } const actual = body.props.auth.user.id; assert.equal(actual, a.id || a.actor_id); (proof.authenticated_actors ||= []).push({ role, id: actual }); save(); } async function logout(page) { await open(page, '/dashboard'); if (page.viewportSize().width < 768) { const toggle = page.getByRole('button', { name: /Toggle sidebar/i, }); if (await toggle.isVisible()) await toggle.click(); } const buttons = page.locator('button[aria-haspopup="menu"]'); for (let i = (await buttons.count()) - 1; i >= 0; i--) { if (!(await buttons.nth(i).isVisible())) continue; await buttons.nth(i).click(); const action = page.locator('[data-test="logout-button"]'); if (await action.isVisible()) { await action.click(); await page.waitForURL(BASE + '/'); return; } await page.keyboard.press('Escape'); } throw Error('Visible logout missing'); } async function shot(page, name, fullPage = true) { assert( !['/login', '/two-factor-challenge'].includes( new URL(page.url()).pathname, ), 'No authentication screenshots', ); const file = path.join(shots, name + '.png'); await page.screenshot({ path: file, fullPage }); proof.screenshots.push(path.relative(ROOT, file).replaceAll('\\', '/')); save(); } async function json(page, url) { const result = await page.evaluate(async route => { const r = await fetch(route, {headers: {Accept: 'application/json'}}); return {status:r.status,target:r.headers.get('X-Investmoz-QA-Target'),data:await r.json()}; }, url); assert.equal(result.target, TARGET); return {status:result.status,data:result.data}; } return { proof, prior, save, phase, browser, context, open, login, logout, shot, json, stamp, receipt, }; } module.exports = { start, BASE, TARGET, DIR, actors, assert, fs, path, sanitized, };