// Scores every notification the wallet can show against Chrome's on-device // "unwanted notification" model — the one that replaces a web notification // with "Possible spam" on Android when its title + body score above 90. // // Not part of the test suite: it launches a throwaway headful-less Chrome // (/usr/bin/google-chrome) that fetches the model from Google's optimization // guide on first use (~10 s, network), shows each line from src/notify-text.js // SAMPLES as a persistent notification on a local page, and reads the score // off chrome://histograms (SafeBrowsing.NotificationContentDetection // .SuspiciousScore, one new sample per notification). Desktop Chrome runs the // model too, flag or no flag; SystemNotifications is off so nothing reaches // the desktop's notification tray. Exit 1 if any line scores 90 or more. // // bun tools/notif-spam-score.js # SAMPLES from src/notify-text.js // bun tools/notif-spam-score.js "Title" "Body" ["Title" "Body" ...] import { spawn } from 'node:child_process'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import puppeteer from 'puppeteer-core'; import { SAMPLES } from '../src/notify-text.js'; const CHROME = process.env.CHROME || '/usr/bin/google-chrome'; const PORT = 8766, DEBUG = 9445, THRESHOLD = 90; const argv = process.argv.slice(2); const items = argv.length ? argv.reduce((a, v, i) => (i % 2 ? a[a.length - 1].push(v) : a.push([v]), a), []) : SAMPLES; const server = Bun.serve({ port: PORT, hostname: '127.0.0.1', fetch(req) { const p = new URL(req.url).pathname; if (p === '/sw.js') return new Response("self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));", { headers: { 'content-type': 'text/javascript' } }); return new Response(`scorescore`, { headers: { 'content-type': 'text/html' } }); } }); const profile = mkdtempSync(join(tmpdir(), 'notif-score-')); const chrome = spawn(CHROME, ['--headless=new', `--user-data-dir=${profile}`, `--remote-debugging-port=${DEBUG}`, '--no-first-run', '--password-store=basic', '--enable-features=ShowWarningsForSuspiciousNotifications', '--disable-features=SystemNotifications', 'about:blank'], { stdio: 'ignore' }); const cleanup = () => { try { chrome.kill(); } catch {} try { server.stop(true); } catch {} try { rmSync(profile, { recursive: true, force: true }); } catch {} }; process.on('exit', cleanup); let browser; for (let i = 0; i < 40 && !browser; i++) { await Bun.sleep(500); browser = await puppeteer.connect({ browserURL: `http://localhost:${DEBUG}`, defaultViewport: null }).catch(() => null); } if (!browser) { console.error('could not reach the scoring Chrome'); process.exit(2); } await browser.defaultBrowserContext().overridePermissions(`http://127.0.0.1:${PORT}`, ['notifications']); const page = await browser.newPage(); await page.goto(`http://127.0.0.1:${PORT}/`, { waitUntil: 'load' }); await page.evaluate(() => navigator.serviceWorker.ready); const hist = await browser.newPage(); async function counts() { await hist.goto('chrome://histograms/SafeBrowsing.NotificationContentDetection.SuspiciousScore', { waitUntil: 'load' }); await Bun.sleep(250); const txt = await hist.evaluate(() => document.body.innerText); const m = {}; for (const line of txt.split('\n')) { const r = /^(\d+)\s+.*\((\d+) = /.exec(line); if (r) m[r[1]] = +r[2]; } const t = /recorded (\d+) samples/.exec(txt); return { m, total: t ? +t[1] : 0 }; } // the model arrives on first use: warm up until one line scores let before = await counts(); for (let i = 0; i < 12 && before.total === 0; i++) { await page.evaluate(() => window.show('warm up', 'the model is still downloading')); await Bun.sleep(5000); before = await counts(); } if (before.total === 0) { console.error('the model never scored anything (offline? no optimization-guide access?)'); process.exit(2); } let worst = 0; for (const [title, body] of items) { await page.evaluate((t, b) => window.show(t, b), title, body); let after, tries = 0; do { await Bun.sleep(600); after = await counts(); } while (after.total === before.total && ++tries < 25); const d = after.total - before.total; const bucket = Object.keys(after.m).filter((k) => (after.m[k] || 0) > (before.m[k] || 0)).join('+'); const score = d === 1 ? Number(bucket) : NaN; worst = Math.max(worst, score || 0); console.log(`${(d === 1 ? String(score) : d === 0 ? '??' : 'ambiguous').padStart(9)} | ${title} | ${body}`); before = after; } await page.close(); await hist.close(); browser.disconnect(); console.log(worst >= THRESHOLD ? `\nFAIL: a line scored ${worst} (warning at >${THRESHOLD - 1})` : `\nok: every line under ${THRESHOLD}, worst ${worst}`); process.exit(worst >= THRESHOLD ? 1 : 0);