fix: reveal pairs oscillating between populated and empty

Two bugs found:
1. Individual name parts ("Ademo"→"John", "Demo"→"Smith") were added
   as reveal pairs, causing partial replacements that corrupted the DOM.
   The smart engine sends combined forms ("Ademo Demo"→"John Smith")
   which the catch-all already handles. Removed individual name entries
   from buildRevealPairs — only emails, usernames, hostnames, phones
   are matched individually.

2. Cache invalidated on every ss:config-updated (including settings-only
   changes like reveal toggle). Now only invalidates when mappings or
   identity actually change.

https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
This commit is contained in:
Claude
2026-03-29 17:20:48 +00:00
parent e9faf1df8e
commit 5e47ed5aae
5 changed files with 22 additions and 13 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Silent Send", "name": "Silent Send",
"version": "0.9.13", "version": "0.9.14",
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Silent Send", "name": "Silent Send",
"version": "0.9.13", "version": "0.9.14",
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"permissions": [ "permissions": [
"storage", "storage",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "silent-send", "name": "silent-send",
"version": "0.9.13", "version": "0.9.14",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"description": "Browser extension that substitutes personal data before sending to AI services", "description": "Browser extension that substitutes personal data before sending to AI services",
+18 -9
View File
@@ -1404,13 +1404,17 @@
// session, preventing false positives (e.g. "user" in AI prose). // session, preventing false positives (e.g. "user" in AI prose).
function buildRevealPairs() { function buildRevealPairs() {
const pairs = []; const pairs = [];
const added = new Set();
// Helper: only add if this substitute was actually sent // Helper: only add if this substitute was actually sent
function addIfUsed(from, to, caseSensitive) { function addIfUsed(from, to, caseSensitive) {
if (!from || !to) return; if (!from || !to) return;
const entry = sessionSubstitutions.get(from.toLowerCase()); const key = from.toLowerCase();
if (added.has(key)) return;
const entry = sessionSubstitutions.get(key);
if (entry) { if (entry) {
pairs.push({ from, to, caseSensitive }); pairs.push({ from, to, caseSensitive });
added.add(key);
} }
} }
@@ -1423,9 +1427,9 @@
for (const e of (identity.emails || [])) { for (const e of (identity.emails || [])) {
addIfUsed(e.substitute, e.real); addIfUsed(e.substitute, e.real);
} }
for (const n of (identity.names || [])) { // For names: DON'T add individual first/last — the smart pattern engine
addIfUsed(n.substitute, n.real); // combines them (e.g. "Ademo Demo" for "John Smith"). The catch-all
} // below picks up the combined form from sessionSubstitutions.
for (const u of (identity.usernames || [])) { for (const u of (identity.usernames || [])) {
addIfUsed(u.substitute, u.real); addIfUsed(u.substitute, u.real);
} }
@@ -1437,10 +1441,13 @@
} }
} }
// Also add auto-detect and auto-redact substitutions from this session // Catch-all: add any session substitution not already covered above.
// This picks up combined names ("Ademo Demo" → "John Smith"),
// auto-detected PII, and auto-redacted secrets.
for (const [key, entry] of sessionSubstitutions) { for (const [key, entry] of sessionSubstitutions) {
if (!pairs.some(p => p.from.toLowerCase() === key)) { if (!added.has(key)) {
pairs.push({ from: entry.replaced, to: entry.original }); pairs.push({ from: entry.replaced, to: entry.original });
added.add(key);
} }
} }
@@ -1448,11 +1455,13 @@
return pairs; return pairs;
} }
// Cache — invalidate when config changes or new substitutions happen // Cache — invalidate when identity/mappings change or new substitutions happen
// Settings-only updates (e.g. reveal toggle) do NOT invalidate
let _revealPairsCache = null; let _revealPairsCache = null;
let _revealPairsCacheSize = 0;
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
if (event.data?.type === 'ss:config-updated') _revealPairsCache = null; if (event.data?.type === 'ss:config-updated') {
if (event.data.mappings || event.data.identity) _revealPairsCache = null;
}
if (event.data?.type === 'ss:substitution-performed') _revealPairsCache = null; if (event.data?.type === 'ss:substitution-performed') _revealPairsCache = null;
}); });
+1 -1
View File
@@ -629,7 +629,7 @@
</section> </section>
<footer> <footer>
<p>Silent Send v0.9.13</p> <p>Silent Send v0.9.14</p>
<p style="font-size:11px;color:#9ca3af;margin-top:6px;max-width:600px"> <p style="font-size:11px;color:#9ca3af;margin-top:6px;max-width:600px">
Silent Send is a convenience tool, not a security guarantee. Third-party sites may change how they send data at any time, which can cause missed substitutions without warning. You are responsible for verifying your data before sending. See the <a href="https://github.com/outis1one/silent-send/blob/main/LICENSE" target="_blank" style="color:#6b7280">LICENSE</a> for full terms. Silent Send is a convenience tool, not a security guarantee. Third-party sites may change how they send data at any time, which can cause missed substitutions without warning. You are responsible for verifying your data before sending. See the <a href="https://github.com/outis1one/silent-send/blob/main/LICENSE" target="_blank" style="color:#6b7280">LICENSE</a> for full terms.
</p> </p>