// Live check of install links, dropped files, the install screen, plugin permissions and the Store // in the running native app (release build started with // WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=). Plain CDP on the shell // webview. X pages are not touched. Whatever this installs is removed again at the end. // // node scripts/store-live-check.mjs import { execFileSync, spawn } from 'node:child_process'; import { existsSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; const [port = '9333', out = '../../verification/store', exe] = process.argv.slice(2); const targets = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json(); const shellTarget = targets.find(t => t.type === 'page' && t.url.startsWith('http://tauri.localhost')); if (!shellTarget) throw new Error('shell webview not found'); const ws = new WebSocket(shellTarget.webSocketDebuggerUrl); let id = 0; const pending = new Map(); ws.onmessage = event => { const msg = JSON.parse(event.data); pending.get(msg.id)?.(msg); pending.delete(msg.id); }; const ready = new Promise(resolve => { ws.onopen = resolve; }); const send = async (method, params = {}) => { await ready; return new Promise(resolve => { const n = ++id; pending.set(n, resolve); ws.send(JSON.stringify({ id: n, method, params })); }); }; const evaluate = async expression => { const result = await send('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true }); if (result.result?.exceptionDetails) throw new Error(result.result.exceptionDetails.exception?.description ?? result.result.exceptionDetails.text); return result.result?.result?.value; }; const invoke = (cmd, args = {}) => evaluate(`window.__TAURI_INTERNALS__.invoke(${JSON.stringify(cmd)}, ${JSON.stringify(args)})`); const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); const shot = async name => { const { result } = await send('Page.captureScreenshot', { format: 'png' }); writeFileSync(join(out, `${name}.png`), Buffer.from(result.data, 'base64')); }; const failures = []; const check = (label, ok, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ` (${detail})` : ''}`); if (!ok) failures.push(label); }; const text = sel => evaluate(`document.querySelector(${JSON.stringify(sel)})?.innerText ?? null`); const waitFor = async (sel, ms = 15000) => { for (let t = 0; t < ms; t += 200) { if (await evaluate(`!!document.querySelector(${JSON.stringify(sel)})`)) return true; await wait(200); } return false; }; const click = sel => evaluate(`(() => { const el = document.querySelector(${JSON.stringify(sel)}); el?.click(); return !!el; })()`); const before = await invoke('plugins_list'); const pluginsDir = process.env.APPDATA ? join(process.env.APPDATA, 'app.bettertwttr.desktop', 'plugins') : null; // 1. A waybetter:// link through the OS path: a second launch with the link as its argument is // forwarded by single-instance to the deep-link handler of the running app. if (exe) { spawn(exe, ['waybetter://install?kind=theme&url=https%3A%2F%2Fwaybetter.fyi%2Fstore%2Fthemes%2Fnord.json'], { detached: true, stdio: 'ignore' }).unref(); const shown = await waitFor('.wbi'); check('deep link: second launch opens the install screen in the running app', shown); await waitFor('.wbi.is-ready, .wbi.is-error', 20000); const state = await evaluate(`document.querySelector('.wbi')?.className`); const body = await text('.wbi'); // waybetter.fyi/store is not deployed yet: the download fails, and nothing is installed. check('deep link: downloaded and shown, or refused with the reason', /is-ready|is-error/.test(state ?? ''), (body ?? '').split('\n').slice(0, 3).join(' / ')); await shot('live-deep-link'); await evaluate(`document.querySelector('.wbi-close, .wbi-state-actions button')?.click()`); await wait(300); // A link to a host that is not allowed never reaches a download. spawn(exe, ['waybetter://install?kind=plugin&url=https%3A%2F%2Fevil.example%2Fx.plugin.js'], { detached: true, stdio: 'ignore' }).unref(); await waitFor('.wbi.is-error'); check('deep link: disallowed host refused', ((await text('.wbi')) ?? '').includes('only https links on waybetter.fyi')); await shot('live-deep-link-refused'); await click('.wbi-state-actions button'); await wait(300); } // 2. A dropped plugin file: the real install screen, native validation, hash, permissions. const source = '/**\n * @name Live Check Badge\n * @description Temporary plugin from scripts/store-live-check.mjs; removed at the end.\n * @version 1.0.0\n * @author wayBetter\n * @permissions ui, network:none\n */\nmodule.exports = class LiveCheckBadge {\n start() { this.scope = window.wayBetter.api.scope(); }\n stop() { this.scope.dispose(); }\n};\n'; await evaluate(`(() => { const dt = new DataTransfer(); dt.items.add(new File([${JSON.stringify(source)}], 'live-check-badge.plugin.js', { type: 'text/javascript' })); const target = document.querySelector('.bt-titlebar') ?? document.body; for (const type of ['dragenter', 'dragover', 'drop']) target.dispatchEvent(new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer: dt })); })()`); check('drop: install screen', await waitFor('.wbi.is-ready')); const screen = (await text('.wbi')) ?? ''; check('drop: validated natively, hash and permissions shown', screen.includes('Live Check Badge') && /[0-9a-f]{64}/.test(screen) && screen.includes('Dialogs') && screen.includes('No network')); check('drop: Install disabled at first', await evaluate(`document.querySelector('.wbi-install')?.disabled === true`)); await wait(1200); await shot('live-install-dropped-plugin'); // Leave it off: uncheck "Turn it on now", then install. await evaluate(`(() => { const box = document.querySelector('.wbi-check input'); if (box?.checked) box.click(); })()`); await click('.wbi-install'); check('drop: installed', await waitFor('.wbi.is-done')); const file = pluginsDir ? join(pluginsDir, 'live-check-badge.plugin.js') : null; check('drop: file written to the plugins folder', file ? existsSync(file) : true, file ?? ''); const after = await invoke('plugins_list'); const installed = after.find(p => p.file === 'live-check-badge.plugin.js'); check('drop: listed with its permissions, off', installed && !installed.enabled && JSON.stringify(installed.permissions) === '["ui","network:none"]'); await click('.wbi-state-actions .bt-deck-secondary'); // Open Plugins await wait(900); await shot('live-plugins-permissions'); // 3. The Store section (the catalog is not deployed yet: the honest offline state). await evaluate(`(() => { const b = [...document.querySelectorAll('.btp-nav button')].find(x => x.textContent.trim() === 'Store'); b?.click(); return !!b; })()`); await wait(2500); const store = (await text('.btp-page')) ?? ''; check('store section renders', store.includes('Store'), store.split('\n').slice(0, 4).join(' / ')); await shot('live-store'); // 4. Clean up: remove the test plugin; the owner's plugins are as before. await invoke('plugin_remove', { file: 'live-check-badge.plugin.js' }); const final = await invoke('plugins_list'); check('cleanup: test plugin removed', !final.some(p => p.file === 'live-check-badge.plugin.js') && (file ? !existsSync(file) : true)); check('cleanup: owner plugins unchanged', JSON.stringify(final.map(p => [p.file, p.enabled])) === JSON.stringify(before.map(p => [p.file, p.enabled]))); await evaluate(`(() => { const b = [...document.querySelectorAll('.btp-nav button')].find(x => x.textContent.trim() === 'Plugins'); b?.click(); })()`); ws.close(); console.log(failures.length ? `\n${failures.length} failure(s): ${failures.join('; ')}` : '\nall checks passed'); process.exit(failures.length ? 1 : 0);