// Run with the bundled Playwright on NODE_PATH. Never uses a personal profile. const { chromium } = require('playwright'); const fs = require('node:fs/promises'); const path = require('node:path'); const root = path.join(__dirname, 'evidence/2026-10-13/fresh-sessions'); const providers = { mullvad:'https://mullvad.net/en', ivpn:'https://www.ivpn.net/en/', proton:'https://protonvpn.com/', nordvpn:'https://nordvpn.com/', surfshark:'https://surfshark.com/' }; const sleep = ms => new Promise(r=>setTimeout(r,ms)); const redact = s => s.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g,'[IPv4 redacted]'); const safeURL = raw => { try { const u=new URL(raw); return {host:u.hostname,path:u.pathname}; } catch { return {}; } }; async function save(name,data) { await fs.writeFile(path.join(root,name),typeof data==='string'?data:JSON.stringify(data,null,2),{mode:0o600}); } async function state(context,page) { const cookies=(await context.cookies()).map(({value,...meta})=>meta); const storage=await page.evaluate(()=>({localStorageKeys:Object.keys(localStorage),sessionStorageKeys:Object.keys(sessionStorage)})).catch(e=>({error:e.message})); const controls=await page.locator('button,input[type=checkbox],[role=switch]').evaluateAll(es=>es.map(e=>({tag:e.tagName,role:e.getAttribute('role'),text:(e.innerText||e.getAttribute('aria-label')||e.name||'').trim(),checked:e.checked??e.getAttribute('aria-checked'),visible:!!(e.getBoundingClientRect().width&&e.getBoundingClientRect().height)}))); return {at:new Date().toISOString(),cookies,storage,controls}; } async function contextFor(browser) {return browser.newContext({locale:'en-US',timezoneId:'UTC',viewport:{width:1280,height:800}});} async function visit(browser,geo,provider,condition,discovery=false) { const context=await contextFor(browser), page=await context.newPage(); const attempt=process.argv[5]||'primary'; const prefix=`${geo}-${provider}-${condition}${attempt==='primary'?'':'-'+attempt}`, url=providers[provider]; const result={geo,provider,condition,attempt,url,browserVersion:browser.version(),freshContext:true,headless:true,locale:'en-US',timezone:'UTC',viewport:{width:1280,height:800},startedAt:new Date().toISOString(),cookiesAtStart:await context.cookies(),phases:[],status:'running'}; const t0=Date.now(), events=[]; let phase='initial', cdp; function event(kind,r,extra={}) {events.push({kind,phase,elapsedMs:Date.now()-t0,...safeURL(r.url()),method:r.method(),resourceType:r.resourceType(),...extra});} page.on('request',r=>event('request',r)); page.on('response',r=>event('response',r.request(),{status:r.status()})); page.on('requestfailed',r=>event('failure',r,{error:r.failure()?.errorText})); try { cdp=await context.newCDPSession(page); await cdp.send('Network.enable'); await cdp.send('Network.setCacheDisabled',{cacheDisabled:true}); try {await page.goto(url,{waitUntil:'domcontentloaded',timeout:20000});}catch(e){result.navigationError=e.message;} if(Date.now()-t0<10000)await sleep(10000-(Date.now()-t0)); let body=await page.locator('body').innerText({timeout:5000}); if(body.length<500&&!/Performing security verification|verif(?:y|ies).*?(?:human|bot)|checking your browser|Just a moment\.\.\./i.test(body)) { result.extraRenderWait=true; await page.waitForFunction(()=>document.body?.innerText.length>=500,null,{timeout:20000}).catch(()=>{}); body=await page.locator('body').innerText({timeout:5000}); } result.before=await state(context,page); result.finalUrl=page.url(); await save(prefix+'-before.txt',redact(body)); await page.screenshot({path:path.join(root,prefix+'-before.png')}); if(/Performing security verification|verif(?:y|ies).*?(?:human|bot)|checking your browser|Just a moment\.\.\./i.test(body)||body.length<500) {result.status='excluded_challenge_or_incomplete';return result;} if(discovery) {result.status='discovery_only';return result;} result.action={requested:condition,completed:false}; if(condition==='refused'||condition==='accepted') { const pat=condition==='refused'?/^(Decline all|Reject all|Reject all cookies|Deny all|Only necessary|Accept necessary)$/i:/^(Accept all|Accept all cookies|Allow all)$/i; let direct=page.getByRole('button',{name:pat}); if(!(await direct.count()===1&&await direct.isVisible())) { for(const name of ['Privacy controls','Cookie preferences','Cookie settings']) { const opener=page.getByRole('button',{name,exact:true}); if(await opener.count()===1&&await opener.isVisible()) { result.action.opened=name;await opener.click({timeout:5000}); result.preferencesBefore=await state(context,page); await save(prefix+'-preferences.txt',redact(await page.locator('body').innerText())); await page.screenshot({path:path.join(root,prefix+'-preferences.png')}); break; } } direct=page.getByRole('button',{name:pat}); } if(await direct.count()===1&&await direct.isVisible()) { result.action.label=await direct.innerText(); await direct.click({timeout:5000}); result.action.completed=true; } else {result.status=result.before.controls.some(c=>c.visible&&/cookie|privacy|preference/i.test(c.text))?'choice_requires_review':'choice_unavailable';return result;} } else result.action.completed=true; phase='comparison'; result.phases.push({name:phase,startElapsedMs:Date.now()-t0}); result.afterAction=await state(context,page); await sleep(60000); result.after=await state(context,page); result.phases.at(-1).endElapsedMs=Date.now()-t0; await save(prefix+'-after.txt',redact(await page.locator('body').innerText())); await page.screenshot({path:path.join(root,prefix+'-after.png')}); if(condition==='refused') { phase='reload';result.phases.push({name:phase,startElapsedMs:Date.now()-t0}); const tr=Date.now();try {await page.reload({waitUntil:'domcontentloaded',timeout:20000});}catch(e){result.reloadError=e.message;} if(Date.now()-tr<10000)await sleep(10000-(Date.now()-tr)); result.afterReload=await state(context,page);result.phases.at(-1).endElapsedMs=Date.now()-t0; await save(prefix+'-reload.txt',redact(await page.locator('body').innerText())); await page.screenshot({path:path.join(root,prefix+'-reload.png')}); } result.status='complete'; } catch(e){result.status='error';result.error=redact(e.message);} finally { result.finishedAt=new Date().toISOString();result.elapsedMs=Date.now()-t0; await save(prefix+'.json',{result,events});await context.close(); console.log(JSON.stringify({geo,provider,condition,status:result.status,elapsedMs:result.elapsedMs,controls:result.before?.controls.filter(c=>c.visible&&/cookie|accept|reject|decline|privacy|preference/i.test(c.text)),cookies:result.after?.cookies.map(c=>c.name)})); } return result; } async function validate(browser) { const a=await contextFor(browser),p=await a.newPage(),seen=[];p.on('response',r=>seen.push({path:new URL(r.url()).pathname,status:r.status()})); await p.goto('http://127.0.0.1:8769/start',{timeout:10000});await p.getByText('Probe completed',{exact:true}).waitFor({timeout:10000}); const first=await state(a,p);await a.close(); const b=await contextFor(browser),q=await b.newPage(); const empty=await b.cookies();await q.goto('http://127.0.0.1:8769/not-found'); const keys=await q.evaluate(()=>Object.keys(localStorage));await b.close(); const ok=seen.some(e=>e.status===302)&&seen.some(e=>e.path==='/probe'&&e.status===204)&&first.cookies.length===2&&empty.length===0&&keys.length===0; await save('collector-validation.json',{at:new Date().toISOString(),browserVersion:browser.version(),ok,seen,first,secondContext:{cookies:empty,localStorageKeys:keys}}); console.log(JSON.stringify({validation:ok}));if(!ok)throw Error('Collector validation failed'); } async function route(browser,geo,label) { const c=await contextFor(browser),p=await c.newPage(),checks=[]; for(const url of ['https://nordvpn.com/what-is-my-ip/','https://surfshark.com/what-is-my-ip']) { try {await p.goto(url,{waitUntil:'domcontentloaded',timeout:20000});await sleep(8000); const text=redact(await p.locator('body').innerText()); await save(`${geo}-route-${label}-${new URL(url).hostname}.txt`,text); const relevant=text.split('\n').filter(l=>/Germany|Deutschland|California|Los Angeles|Kenya|Nairobi|France|Country:|City:|Location:|United States/i.test(l)); checks.push({url,relevant}); }catch(e){checks.push({url,error:redact(e.message)});} } try { await p.goto('https://ipinfo.io/json',{waitUntil:'domcontentloaded',timeout:15000}); const raw=JSON.parse(await p.locator('body').innerText()); checks.push({url:'https://ipinfo.io/json',usable:!!raw.country,country:raw.country,region:raw.region,city:raw.city}); if(!raw.country) { await p.goto('https://ipwho.is/',{waitUntil:'domcontentloaded',timeout:15000}); const fallback=JSON.parse(await p.locator('body').innerText()); checks.push({url:'https://ipwho.is/',usable:fallback.success===true,country:fallback.country,countryCode:fallback.country_code,region:fallback.region,city:fallback.city}); } }catch(e){checks.push({url:'https://ipinfo.io/json',error:redact(e.message)});} await save(`${geo}-route-${label}.json`,{geo,at:new Date().toISOString(),checks});console.log(JSON.stringify({geo,route:checks}));await c.close(); } async function inspect(browser,geo,provider,refuseFirst=false) { const c=await contextFor(browser),p=await c.newPage(); const name=`${geo}-${provider}-${refuseFirst?'persistence':'interface'}`; try { await p.goto(providers[provider],{waitUntil:'domcontentloaded',timeout:20000});await sleep(10000); await p.waitForFunction(()=>document.body?.innerText.length>=500,null,{timeout:20000}).catch(()=>{}); if(refuseFirst) { const reject=p.getByRole('button',{name:/^(Decline all|Reject all)$/i}); if(!await reject.count()) { for(const label of ['Privacy controls','Cookie preferences','Cookie settings']) { const open=p.getByRole('button',{name:label,exact:true}); if(await open.count()===1&&await open.isVisible()) {await open.click();break;} } } await reject.click({timeout:5000}); await p.reload({waitUntil:'domcontentloaded',timeout:20000});await sleep(10000); } await save(name+'-before.txt',redact(await p.locator('body').innerText())); const possible=p.getByRole('button',{name:/^(Privacy controls|Cookie preferences|Cookie settings)$/i}); const candidates=await possible.all(); let opened; for(const button of candidates)if(await button.isVisible()) {opened=await button.innerText();await button.click();break;} await save(name+'.txt',redact(await p.locator('body').innerText())); const controls=await state(c,p); await p.screenshot({path:path.join(root,name+'.png')}); await save(name+'.json',{geo,provider,at:new Date().toISOString(),freshContext:true,refuseFirst,opened,controls}); console.log(JSON.stringify({geo,provider,opened,controls:controls.controls.filter(x=>x.visible)})); }catch(e) { await save(name+'-failed.json',{geo,provider,at:new Date().toISOString(),status:'failed',error:redact(e.message),body:redact(await p.locator('body').innerText().catch(()=>''))}); await p.screenshot({path:path.join(root,name+'-failed.png')}).catch(()=>{});throw e; }finally {await c.close();} } (async()=>{ await fs.mkdir(root,{recursive:true,mode:0o700}); const browser=await chromium.launch({channel:'chrome',headless:true,chromiumSandbox:true,ignoreDefaultArgs:true,args:['--headless=new','--remote-debugging-pipe','--no-first-run','--no-default-browser-check']}); try { const [mode,geo='DE',provider]=process.argv.slice(2); if(mode==='validate')await validate(browser); else if(mode==='route')await route(browser,geo,provider||'start'); else if(mode==='finish') {try{await inspect(browser,geo,provider||'surfshark',true);}catch(e){console.log(JSON.stringify({confirmation:'failed',message:redact(e.message)}));}await route(browser,geo,'end');} else if(mode==='discover')for(const id of provider?[provider]:Object.keys(providers))await visit(browser,geo,id,'discovery',true); else if(mode==='inspect'||mode==='persistence')await inspect(browser,geo,provider,mode==='persistence'); else if(mode==='run')for(const id of provider?[provider]:Object.keys(providers)) { const base=await visit(browser,geo,id,'untouched'); if(base.status!=='complete')continue; const hasChoice=base.after.controls.some(c=>c.visible&&/cookie|privacy|preference|accept all|decline all|reject all/i.test(c.text)); if(hasChoice)for(const cond of ['refused','accepted'])await visit(browser,geo,id,cond); else await save(`${geo}-${id}-availability.json`,{geo,provider:id,at:new Date().toISOString(),refused:'unavailable_no_visible_consent_interface_in_untouched_visit',accepted:'unavailable_no_visible_consent_interface_in_untouched_visit'}); } else throw Error('Mode: validate, route, discover, run'); } finally {await browser.close();} })().catch(e=>{console.error(e);process.exitCode=1});