diff --git a/README.md b/README.md index c87deb2..54702b8 100644 --- a/README.md +++ b/README.md @@ -67,17 +67,13 @@ A browser extension (Chrome, Firefox, and Safari) that intercepts personal infor | `123-45-6789` | `[REDACTED-SSN]` | | `4111 1111 1111 1111` | `[REDACTED-CARD]` | -### Proper noun detection (automatic) +### Proper noun detection (opt-in) -The auto-detect scanner also catches capitalized words mid-sentence that might be names, company names, or project names you forgot to configure. For example: +The auto-detect scanner can optionally flag capitalized phrases that might be names, company names, or project names you forgot to configure. **Disabled by default** because it can produce false positives on normal phrases like "Getting Started" or "Generate Design". -| You type | What happens | -|----------|-------------| -| `...talked to Sarah about the deploy` | Flags "Sarah" as a possible name | -| `...the Acme Corp internal API` | Flags "Acme Corp" as a possible organization | -| `...pushed to Project Atlas staging` | Flags "Project Atlas" as a possible project name | +Enable it in the popup → Options tab → **Detect proper nouns**. -These are flagged as warnings (not auto-redacted) so you can decide whether to add them as mappings. Common English words, programming terms, days, and months are excluded to reduce false positives. +When enabled, phrases like "Acme Corp" or "Project Atlas" will be flagged as warnings so you can decide whether to add them as mappings. You can click "ignore" on any false positive to permanently dismiss it. ### Bulk import (speed up setup) @@ -402,6 +398,20 @@ src/ ## Sync features +Keep your identities, mappings, and settings in sync across browsers and devices. + +### Sync methods + +| Method | How it works | Chrome | Firefox | Brave | Safari | +|---|---|---|---|---|---| +| **Sync Code** | Generate a code, paste in another browser | Yes | Yes | Yes | Yes | +| **GitHub Gist** | Store settings in a private Gist (needs a free GitHub PAT) | Yes | Yes | Yes | Yes | +| **Custom URL** | Any endpoint supporting GET + PUT (WebDAV, cloud function, etc.) | Yes | Yes | Yes | Yes | +| **Browser account sync** | Automatic via your Chrome/Firefox account | Yes | Yes | No | No | +| **Auto-Sync Folder** | Pick a cloud-synced folder (Dropbox, OneDrive, etc.) | Yes | No | No | No | + +Auto-Sync Folder uses the File System Access API which is only available in Chrome. All other methods work in every browser. + ### Auto sync When configured, the extension automatically pushes and pulls settings on a configurable interval (5/15/30/60 minutes) using GitHub Gist or a custom URL endpoint. Local changes trigger an immediate push. diff --git a/manifest.firefox.json b/manifest.firefox.json index f0c056e..f1e5987 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "2.0.5", + "version": "2.0.6", "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 ad9d1fa..4890862 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "2.0.5", + "version": "2.0.6", "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 e33f7f7..454ba73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-send", - "version": "2.0.5", + "version": "2.0.6", "private": true, "license": "BSL-1.1", "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 47fd4e5..24eef4d 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -604,9 +604,11 @@ } // Proper noun heuristic — catch names, company names, project names - // that aren't configured in identity - const properNouns = detectProperNouns(text, configured); - findings.push(...properNouns); + // Disabled by default (too many false positives). Enable in Options. + if (settings.detectProperNouns) { + const properNouns = detectProperNouns(text, configured); + findings.push(...properNouns); + } // Deduplicate by value const seen = new Set(); @@ -906,14 +908,60 @@ return SKIP_URL_PATTERNS.some(p => p.test(url)); } - window.fetch = async function (url, options) { + window.fetch = async function (input, init) { if (!settings.enabled || !hasSubstitutions()) { - return originalFetch.call(this, url, options); + return originalFetch.call(this, input, init); + } + + // Handle both fetch(url, options) and fetch(Request) signatures + let url, options; + if (input instanceof Request) { + url = input.url; + // Clone the Request so we can read/modify the body + options = { + method: input.method, + headers: input.headers, + body: null, // will read below + mode: input.mode, + credentials: input.credentials, + cache: input.cache, + redirect: input.redirect, + referrer: input.referrer, + signal: input.signal, + }; + // Read the body from the Request object + try { + const ct = input.headers.get('content-type') || ''; + if (ct.includes('json') || ct.includes('text')) { + options.body = await input.text(); + } else { + // Non-text body — pass through unmodified + return originalFetch.call(this, input, init); + } + } catch { + return originalFetch.call(this, input, init); + } + } else { + url = input; + options = init ? { ...init } : {}; } const urlStr = typeof url === 'string' ? url : url?.url || ''; const method = (options?.method || 'GET').toUpperCase(); + // Convert non-string bodies to string where possible + if (options.body && typeof options.body !== 'string' && !(options.body instanceof FormData)) { + try { + if (options.body instanceof Blob) { + options.body = await options.body.text(); + } else if (options.body instanceof ArrayBuffer || ArrayBuffer.isView(options.body)) { + options.body = new TextDecoder().decode(options.body); + } else if (options.body instanceof URLSearchParams) { + options.body = options.body.toString(); + } + } catch { /* leave as-is */ } + } + // Only intercept POST/PUT/PATCH with a body if ( (method === 'POST' || method === 'PUT' || method === 'PATCH') && diff --git a/src/lib/auto-detect.js b/src/lib/auto-detect.js index bea7464..e44ac75 100644 --- a/src/lib/auto-detect.js +++ b/src/lib/auto-detect.js @@ -136,7 +136,7 @@ const AutoDetect = { * * Returns array of { name, value, hint, category, index } */ - scan(text, identity) { + scan(text, identity, options) { if (!text || text.length < 5) return []; const hasContext = CONTEXT_WORDS.test(text); @@ -194,8 +194,11 @@ const AutoDetect = { } // Proper noun heuristic — catch names, company names, project names - const properNouns = this._detectProperNouns(text, configured); - findings.push(...properNouns); + // Disabled by default (too many false positives). Pass detectProperNouns: true to enable. + if (options?.detectProperNouns) { + const properNouns = this._detectProperNouns(text, configured); + findings.push(...properNouns); + } // Deduplicate overlapping matches findings.sort((a, b) => (a.index || 0) - (b.index || 0)); diff --git a/src/lib/sync.js b/src/lib/sync.js index 80c743a..b6a6b4e 100644 --- a/src/lib/sync.js +++ b/src/lib/sync.js @@ -791,25 +791,41 @@ const SilentSendSync = { // ---------------------------------------------------------------- async _getAllData() { - const result = await api.storage.local.get(null); + // Use dynamic import to avoid circular dependency + const StorageModule = (await import('./storage.js')).default; + const identity = await StorageModule._readSecure('ss_identity'); + const mappings = await StorageModule._readSecure('ss_mappings'); + const settings = await StorageModule._readSecure('ss_settings'); + const result = await api.storage.local.get('ss_lastModified'); return { version: '1', lastModified: result.ss_lastModified || Date.now(), - identity: result.ss_identity || {}, - mappings: result.ss_mappings || [], - settings: result.ss_settings || {}, + identity: identity || {}, + mappings: mappings || [], + settings: settings || {}, }; }, async _applyData(data, source = 'unknown') { - const toSet = { + const StorageModule = (await import('./storage.js')).default; + + // Write through Storage module so data gets encrypted if at-rest + // encryption is enabled + if (data.identity !== undefined) { + await StorageModule._writeSecure('ss_identity', data.identity); + } + if (data.mappings !== undefined) { + await StorageModule._writeSecure('ss_mappings', data.mappings); + } + if (data.settings !== undefined) { + await StorageModule._writeSecure('ss_settings', data.settings); + } + + // Metadata stays plaintext + await api.storage.local.set({ ss_lastModified: data.lastModified, ss_sync_notification: { source, time: Date.now() }, - }; - if (data.identity !== undefined) toSet.ss_identity = data.identity; - if (data.mappings !== undefined) toSet.ss_mappings = data.mappings; - if (data.settings !== undefined) toSet.ss_settings = data.settings; - await api.storage.local.set(toSet); + }); }, async _getSyncChunkKeys() { diff --git a/src/options/options.html b/src/options/options.html index ea8ce0a..a4291ff 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -238,7 +238,7 @@
-Pick the same folder in each browser once. Changes are written to @@ -278,9 +278,11 @@
- Automatically push and pull settings on an interval. Uses the Gist or URL method configured above. + Automatically push and pull settings in the background on a schedule. + Uses GitHub Gist or Custom URL — configure one of those above first, then enable auto sync here. + Changes you make locally are pushed immediately; remote changes are pulled on the interval.
Sync, encryption, org, version history, import/export, and more
diff --git a/src/popup/popup.js b/src/popup/popup.js index 2f9e75d..3743790 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -225,6 +225,7 @@ async function initUnlockedUI() { $('#optAutoRedact').checked = settings.autoRedactDetected !== false; $('#optHighlights').checked = settings.showHighlights || false; $('#optDocPreview').checked = settings.docScanPreview !== false; + $('#optProperNouns').checked = settings.detectProperNouns || false; // Options tab change handlers const optHandlers = [ @@ -233,6 +234,7 @@ async function initUnlockedUI() { ['optAutoRedact', 'autoRedactDetected'], ['optHighlights', 'showHighlights'], ['optDocPreview', 'docScanPreview'], + ['optProperNouns', 'detectProperNouns'], ]; for (const [id, key] of optHandlers) { $(`#${id}`).addEventListener('change', async (e) => {