From 61f1e774303dcbaa678cc9f40d9237c30a341703 Mon Sep 17 00:00:00 2001 From: Ruslan Strazhnyk Date: Tue, 7 Apr 2026 21:01:09 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Improved=20probe=20counting=20=E2=80=94?= =?UTF-8?q?=20periodic=20perf=20scan=20+=20XHR=20intercept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LinkedIn saves window.fetch at parse time, bypassing our Proxy. Added: - Periodic performance.getEntriesByType scan (catches chrome-extension:// resource entries the PerformanceObserver may miss) - Deduplicated counting via Set to avoid double-counting - XMLHttpRequest.open intercept (in case LinkedIn falls back to XHR) - Bumped extension ID capture limit from 20 to 50 Co-Authored-By: Claude Opus 4.6 (1M context) --- content.js | 42 ++++++++++++++++++++++++++++++++++++------ eslint.config.js | 2 ++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/content.js b/content.js index 746a377..780da99 100644 --- a/content.js +++ b/content.js @@ -64,15 +64,45 @@ }); Object.defineProperty(window, 'fetch', { value: fetchProxy, writable: true, configurable: true }); - // ── 2. PerformanceObserver (backup counting) ──────────────────────── - try { - const po = new PerformanceObserver((list) => { - for (const entry of list.getEntries()) { - if (entry.name && entry.name.includes('chrome-extension://')) probeCount++; + // ── 2. PerformanceObserver + periodic scan for extension probes ───── + const seenEntries = new Set(); + function countPerfEntries(entries) { + for (const entry of entries) { + if (entry.name && entry.name.includes('chrome-extension://') && !seenEntries.has(entry.name)) { + seenEntries.add(entry.name); + probeCount++; + if (probedExtensions.length < 50) { + const m = entry.name.match(/chrome-extension:\/\/([^/]+)/); + if (m && m[1] !== 'invalid') probedExtensions.push(m[1]); + } } - }); + } + } + try { + const po = new PerformanceObserver((list) => countPerfEntries(list.getEntries())); po.observe({ type: 'resource', buffered: true }); } catch (_e) {} + // Periodic scan catches entries the observer may have missed + setInterval(() => { + try { + countPerfEntries(performance.getEntriesByType('resource')); + } catch (_e) {} + }, 2000); + + // ── 2b. Intercept XMLHttpRequest (LinkedIn may use XHR for probes) ── + const nativeXHROpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function (method, url, ...rest) { + if (typeof url === 'string' && (url.includes('chrome-extension://') || url.includes('moz-extension://'))) { + probeCount++; + if (probedExtensions.length < 50) { + const m = url.match(/(?:chrome|moz)-extension:\/\/([^/]+)/); + if (m && m[1] !== 'invalid') probedExtensions.push(m[1]); + } + // Abort silently — don't actually send + return; + } + return nativeXHROpen.call(this, method, url, ...rest); + }; // ── 3. Fingerprint spoofing ───────────────────────────────────────── try { diff --git a/eslint.config.js b/eslint.config.js index 2253169..5aca651 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -22,6 +22,8 @@ export default [ Promise: 'readonly', MutationObserver: 'readonly', PerformanceObserver: 'readonly', + XMLHttpRequest: 'readonly', + performance: 'readonly', URL: 'readonly', // Chrome Extension chrome: 'readonly',