diff --git a/manifest.firefox.json b/manifest.firefox.json index 917c622..0f9077d 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "2.0.11", + "version": "2.0.13", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "browser_specific_settings": { "gecko": { diff --git a/manifest.json b/manifest.json index 608598c..9800648 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "2.0.11", + "version": "2.0.13", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "permissions": [ "storage", diff --git a/package.json b/package.json index b78dfe4..0a2ff37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-send", - "version": "2.0.11", + "version": "2.0.13", "private": true, "license": "BSL-1.1", "description": "Browser extension that substitutes personal data before sending to AI services", diff --git a/src/background/service-worker.js b/src/background/service-worker.js index d18a2a5..ffbdf9f 100644 --- a/src/background/service-worker.js +++ b/src/background/service-worker.js @@ -201,6 +201,17 @@ const messageHandlers = { await setupAutoSyncAlarm(); }, + async 'get:decrypted-config'(_message, _sender, sendResponse) { + try { + const mappings = await Storage.getMappings(); + const identity = await Storage.getIdentity(); + const settings = await Storage.getSettings(); + sendResponse({ mappings, identity, settings }); + } catch { + sendResponse(null); + } + }, + async 'org:config-changed'() { await setupOrgPolicyAlarm(); }, diff --git a/src/content/content.js b/src/content/content.js index f8d578a..c7e5031 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -905,11 +905,8 @@ // ============================================================ // Fetch Interception — scans ALL POST requests with a body. // Service-agnostic: doesn't depend on URL patterns. - // Uses __ssOriginalFetch from the early hook (injected synchronously - // before any page JS) to ensure we have the real fetch, even if - // frameworks like Next.js (ChatGPT) store a reference early. // ============================================================ - const originalFetch = window.__ssOriginalFetch || window.fetch; + const originalFetch = window.fetch; // URLs to never touch (static assets, analytics, etc.) const SKIP_URL_PATTERNS = [ @@ -1029,9 +1026,7 @@ return originalFetch.call(this, url, options); }; - // Register our fetch interceptor so the early hook proxy can use it - window.__ssInterceptFetch = window.fetch; - window.__ssReady = true; + // ============================================================ // Document Upload Processing @@ -1397,8 +1392,8 @@ // ============================================================ // XMLHttpRequest Interception — same aggressive approach // ============================================================ - const origOpen = window.__ssOriginalXHROpen || XMLHttpRequest.prototype.open; - const origSend = window.__ssOriginalXHRSend || XMLHttpRequest.prototype.send; + const origOpen = XMLHttpRequest.prototype.open; + const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._ssUrl = url; diff --git a/src/content/injector.js b/src/content/injector.js index e04dc63..0e70c7b 100644 --- a/src/content/injector.js +++ b/src/content/injector.js @@ -15,24 +15,6 @@ if (window.__silentSendInjected) return; window.__silentSendInjected = true; - // Cross-browser API - const api = - typeof browser !== 'undefined' && browser.runtime - ? browser - : typeof chrome !== 'undefined' - ? chrome - : null; - - // IMMEDIATELY inject the early fetch hook into the page world as an - // EXTERNAL file. Must be external (not inline) because sites like - // claude.ai have strict CSP that blocks inline scripts. Firefox - // enforces this strictly; Chrome is more permissive but external - // works everywhere. - const earlyHook = document.createElement('script'); - earlyHook.src = api.runtime.getURL('src/content/early-hook.js'); - (document.head || document.documentElement).appendChild(earlyHook); - earlyHook.onload = () => earlyHook.remove(); - // Merge active profiles into flat identity object function mergeProfiles(data) { const profiles = data?.profiles || []; @@ -65,23 +47,57 @@ return merged; } + // Cross-browser API + const api = + typeof browser !== 'undefined' && browser.runtime + ? browser + : typeof chrome !== 'undefined' + ? chrome + : null; + // Load mappings and settings, then inject into page async function init() { + let mappings, identity, settings; + const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']); - const settings = result.ss_settings || { enabled: true }; + const isEncrypted = result.ss_mappings?._ssLocalEncrypted || + result.ss_identity?._ssLocalEncrypted; - // Check if data is encrypted (locked) — pass empty config - // The background will send decrypted data via vault:unlocked when ready - const isLocked = result.ss_mappings?._ssLocalEncrypted || - result.ss_identity?._ssLocalEncrypted; - const mappings = isLocked ? [] : (result.ss_mappings || []); - const identityData = isLocked ? {} : (result.ss_identity || {}); + if (isEncrypted) { + // Data is encrypted — ask the background script for decrypted config. + // The background has access to the Storage module which can decrypt. + try { + const response = await api.runtime.sendMessage({ type: 'get:decrypted-config' }); + if (response?.mappings) { + mappings = response.mappings; + identity = response.identity || {}; + settings = response.settings || { enabled: true }; + } else { + // Background couldn't decrypt (locked) — inject with empty config + // and wait for vault:unlocked message later + mappings = []; + identity = {}; + settings = result.ss_settings || { enabled: true }; + } + } catch { + mappings = []; + identity = {}; + settings = result.ss_settings || { enabled: true }; + } + } else { + // Data is plaintext — read directly + mappings = result.ss_mappings || []; + const identityData = result.ss_identity || {}; + identity = mergeProfiles(identityData); + settings = result.ss_settings || { enabled: true }; + } - // Merge active profiles into a flat identity object for the content script - const identity = mergeProfiles(identityData); + // Ensure identity is merged if it came from background + if (identity.profiles) { + identity = mergeProfiles(identity); + } - // Load the full content.js which will use __ssOriginalFetch - // (captured by the early hook above) and set __ssReady = true + // Inject the main interception script into the page's world const script = document.createElement('script'); script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings })); script.src = api.runtime.getURL('src/content/content.js'); diff --git a/src/options/options.html b/src/options/options.html index 7355dcd..b609f97 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -591,7 +591,7 @@