diff --git a/manifest.firefox.json b/manifest.firefox.json index dc9888f..ae8355f 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.9.42", + "version": "0.9.43", "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 2dec8c9..425b3ec 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.9.42", + "version": "0.9.43", "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 3213a9e..0baad02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-send", - "version": "0.9.42", + "version": "0.9.43", "private": true, "license": "MIT", "description": "Browser extension that substitutes personal data before sending to AI services", diff --git a/src/content/content.js b/src/content/content.js index 552fd65..0702685 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -50,7 +50,7 @@ const sorted = [...maps].sort((a, b) => b.real.length - a.real.length); for (const m of sorted) { - if (!m.enabled || !m.real || !m.substitute) continue; + if (!m.enabled || !m.real?.trim() || !m.substitute?.trim()) continue; const escaped = esc(m.real); const regex = new RegExp(escaped, m.caseSensitive ? 'g' : 'gi'); let match; @@ -70,7 +70,7 @@ let result = text; const sorted = [...maps].sort((a, b) => b.substitute.length - a.substitute.length); for (const m of sorted) { - if (!m.enabled || !m.real || !m.substitute) continue; + if (!m.enabled || !m.real?.trim() || !m.substitute?.trim()) continue; const escaped = esc(m.substitute); const regex = new RegExp(escaped, m.caseSensitive ? 'g' : 'gi'); result = result.replace(regex, m.real); @@ -825,67 +825,74 @@ } window.__ssInterceptFetch = async function (url, options) { - if (!settings.enabled || !hasSubstitutions()) { + // Top-level guard: any unhandled error must never break the original request. + try { + if (!settings.enabled || !hasSubstitutions()) { + return originalFetch.call(this, url, options); + } + + const urlStr = typeof url === 'string' ? url : url?.url || ''; + const method = (options?.method || 'GET').toUpperCase(); + + // Only intercept POST/PUT/PATCH + if ( + (method === 'POST' || method === 'PUT' || method === 'PATCH') && + options?.body && !shouldSkipUrl(urlStr) + ) { + // FormData body — scan file uploads via DocumentScanner + if (options.body instanceof FormData && typeof globalThis.DocumentScanner !== 'undefined') { + try { + const scannedForm = await scanFormData(options.body); + if (scannedForm) { + options = { ...options, body: scannedForm.formData }; + if (scannedForm.replacements.length > 0) { + notifySubstitutions(scannedForm.replacements); + console.log( + `[Silent Send] Substituted ${scannedForm.replacements.length} value(s) in file upload to ${urlStr}` + ); + } + } + } catch (e) { + console.warn('[Silent Send] FormData scan failed:', e); + } + } + + // String body — JSON or raw text + if (typeof options.body === 'string') { + try { + // Try JSON + const body = JSON.parse(options.body); + const { modified, replacements } = processBody(body); + + if (modified) { + options = { ...options, body: JSON.stringify(body) }; + notifySubstitutions(replacements); + console.log( + `[Silent Send] Substituted ${replacements.length} value(s) in ${urlStr}` + ); + } + } catch (e) { + // Not JSON — try raw string substitution (form data, etc.) + if (options.body.length > MIN_STRING_LENGTH) { + const result = substituteAll(options.body); + if (result.modified) { + options = { ...options, body: result.text }; + notifySubstitutions(result.replacements); + console.log( + `[Silent Send] Substituted ${result.replacements.length} value(s) in form body` + ); + } + } + } + } + } + + return originalFetch.call(this, url, options); + } catch (e) { + // Something went wrong in Silent Send — never block the original request + console.warn('[Silent Send] Fetch interceptor error, passing through:', e); return originalFetch.call(this, url, options); } - - const urlStr = typeof url === 'string' ? url : url?.url || ''; - const method = (options?.method || 'GET').toUpperCase(); - - // Only intercept POST/PUT/PATCH - if ( - (method === 'POST' || method === 'PUT' || method === 'PATCH') && - options?.body && !shouldSkipUrl(urlStr) - ) { - // FormData body — scan file uploads via DocumentScanner - if (options.body instanceof FormData && typeof globalThis.DocumentScanner !== 'undefined') { - try { - const scannedForm = await scanFormData(options.body); - if (scannedForm) { - options = { ...options, body: scannedForm.formData }; - if (scannedForm.replacements.length > 0) { - notifySubstitutions(scannedForm.replacements); - console.log( - `[Silent Send] Substituted ${scannedForm.replacements.length} value(s) in file upload to ${urlStr}` - ); - } - } - } catch (e) { - console.warn('[Silent Send] FormData scan failed:', e); - } - } - - // String body — JSON or raw text - if (typeof options.body === 'string') { - try { - // Try JSON - const body = JSON.parse(options.body); - const { modified, replacements } = processBody(body); - - if (modified) { - options = { ...options, body: JSON.stringify(body) }; - notifySubstitutions(replacements); - console.log( - `[Silent Send] Substituted ${replacements.length} value(s) in ${urlStr}` - ); - } - } catch (e) { - // Not JSON — try raw string substitution (form data, etc.) - if (options.body.length > MIN_STRING_LENGTH) { - const result = substituteAll(options.body); - if (result.modified) { - options = { ...options, body: result.text }; - notifySubstitutions(result.replacements); - console.log( - `[Silent Send] Substituted ${result.replacements.length} value(s) in form body` - ); - } - } - } - } - } - - return originalFetch.call(this, url, options); }; // Activate the fetch hook. If early-hook.js ran first (world: MAIN, document_start), @@ -1130,6 +1137,7 @@ for (const p of pairs) { const searchTerm = settings.revealMode ? p.to : p.from; + if (!searchTerm?.trim()) continue; const escaped = esc(searchTerm); // Add word boundaries when the term starts/ends with word chars to // prevent partial-word matches (e.g. "aud" inside "Claude") diff --git a/src/lib/substitution-engine.js b/src/lib/substitution-engine.js index f7e5fd9..d0e64ec 100644 --- a/src/lib/substitution-engine.js +++ b/src/lib/substitution-engine.js @@ -21,7 +21,7 @@ const SubstitutionEngine = { ); for (const mapping of sorted) { - if (!mapping.enabled || !mapping.real || !mapping.substitute) continue; + if (!mapping.enabled || !mapping.real?.trim() || !mapping.substitute?.trim()) continue; const escaped = this._escapeRegex(mapping.real); const regex = new RegExp(escaped, mapping.caseSensitive ? 'g' : 'gi'); @@ -54,7 +54,7 @@ const SubstitutionEngine = { ); for (const mapping of sorted) { - if (!mapping.enabled || !mapping.real || !mapping.substitute) continue; + if (!mapping.enabled || !mapping.real?.trim() || !mapping.substitute?.trim()) continue; const escaped = this._escapeRegex(mapping.substitute); const regex = new RegExp(escaped, mapping.caseSensitive ? 'g' : 'gi'); @@ -71,7 +71,7 @@ const SubstitutionEngine = { const found = []; for (const mapping of mappings) { - if (!mapping.enabled || !mapping.real) continue; + if (!mapping.enabled || !mapping.real?.trim()) continue; const escaped = this._escapeRegex(mapping.real); const regex = new RegExp(escaped, mapping.caseSensitive ? 'g' : 'gi'); @@ -103,7 +103,7 @@ const SubstitutionEngine = { // Collect all match positions in the original text const matches = []; for (const mapping of sorted) { - if (!mapping.enabled || !mapping.real || !mapping.substitute) continue; + if (!mapping.enabled || !mapping.real?.trim() || !mapping.substitute?.trim()) continue; const escaped = this._escapeRegex(mapping.real); const regex = new RegExp(escaped, mapping.caseSensitive ? 'g' : 'gi'); diff --git a/src/options/options.js b/src/options/options.js index 313aedf..f78374a 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -1814,11 +1814,12 @@ async function applyBulkImport() { await Storage.updateProfile(profile.id, profile); } - // Add mappings + // Add mappings — skip any with blank/whitespace-only real values for (const m of result.mappings) { + if (!m.real?.trim()) continue; await Storage.addMapping({ - real: m.real, - substitute: m.substitute || '', + real: m.real.trim(), + substitute: m.substitute?.trim() || '', category: m.category || 'general', caseSensitive: false, });