feat: pre-send PPI detection — warns while typing, auto-add mappings

Like spellcheck for privacy. Scans text as you type and paste
into chat inputs (debounced 800ms). Shows a dark floating warning
panel listing detected PPI BEFORE you hit Enter.

Each detected item has a green [+] button that instantly:
1. Generates a plausible fake value (random IP, fake address, etc.)
2. Adds it as a mapping to storage
3. Shows a checkmark to confirm

The warning disappears when you clear the text or when all
detected items have been addressed.

Three new Options toggles:
- Auto-detect unconfigured PPI (on by default)
- Offer to auto-add detected PPI (on by default)

Also adds a storage bridge (postMessage) so the page-world
content script can read/write chrome.storage through the
injector.

https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
Claude
2026-03-26 04:44:12 +00:00
parent e6b3ef7e39
commit 3cb06c6dea
6 changed files with 289 additions and 5 deletions
+84
View File
@@ -136,6 +136,90 @@
font-style: italic;
}
/* Pre-send PPI warning (spellcheck-style, appears while typing) */
.ss-presend-warning {
position: fixed;
top: 16px;
right: 16px;
max-width: 420px;
background: #1a1a1a;
color: #e5e7eb;
border: 1px solid #f59e0b;
border-radius: 10px;
padding: 12px 16px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
z-index: 999999;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s, transform 0.2s;
pointer-events: none;
}
.ss-presend-warning.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.ss-ps-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 0;
border-bottom: 1px solid #333;
}
.ss-ps-item:last-of-type { border-bottom: none; }
.ss-ps-type {
font-size: 9px;
font-weight: 600;
color: #f59e0b;
min-width: 65px;
text-transform: uppercase;
}
.ss-ps-value {
font-family: 'SF Mono', Monaco, monospace;
font-size: 11px;
color: #4ade80;
background: #0a0a0a;
padding: 2px 6px;
border-radius: 4px;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ss-ps-hint {
font-size: 10px;
color: #9ca3af;
flex: 1;
}
.ss-ps-add {
background: none;
border: 1px solid #4ade80;
border-radius: 4px;
color: #4ade80;
font-size: 14px;
font-weight: bold;
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 0;
}
.ss-ps-add:hover { background: rgba(74, 222, 128, 0.15); }
.ss-ps-add:disabled { border-color: #333; cursor: default; }
/* Floating reveal mode indicator */
.ss-reveal-badge {
position: fixed;
+171 -5
View File
@@ -1006,15 +1006,181 @@
}
// ============================================================
// Input Highlighting
// Pre-Send PPI Detection — scans as you type/paste (spellcheck style)
// ============================================================
// Generate plausible fake values for detected PPI
function generateFake(type, value) {
switch (type) {
case 'Private IP':
case 'Public IP':
return '10.' + rnd(1,254) + '.' + rnd(1,254) + '.' + rnd(1,254);
case 'MAC Address':
return Array.from({length:6}, () => rnd(0,255).toString(16).padStart(2,'0')).join(':');
case 'Street Address':
const streets = ['Oak', 'Maple', 'Pine', 'Cedar', 'Elm', 'Main', 'Park', 'Lake'];
const types = ['St', 'Ave', 'Dr', 'Ln', 'Rd'];
return rnd(100,9999) + ' ' + streets[rnd(0,7)] + ' ' + types[rnd(0,4)];
case 'GPS Coordinates':
return (rnd(-90,90) + Math.random()).toFixed(6) + ',' + (rnd(-180,180) + Math.random()).toFixed(6);
case 'Date (possible DOB)':
return (rnd(1,12) + '').padStart(2,'0') + '/' + (rnd(1,28) + '').padStart(2,'0') + '/' + rnd(1950,2005);
case 'EIN / Tax ID':
return rnd(10,99) + '-' + (rnd(1000000,9999999) + '');
case 'Home Path': {
const fakeUser = 'user' + rnd(100,999);
if (value.startsWith('C:\\')) return 'C:\\Users\\' + fakeUser;
if (value.startsWith('/Users/')) return '/Users/' + fakeUser;
return '/home/' + fakeUser;
}
case 'Shell Prompt':
return 'user@computer:$ ';
case 'Git Remote':
return value.replace(/[:/][^/\s]+\//, ':/anonymous/');
case 'Env Variable':
return value.split('=')[0] + '=REDACTED';
default:
return '[REDACTED]';
}
}
function rnd(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Pre-send warning UI
let preSendWarningEl = null;
let preSendTimer = null;
function showPreSendWarning(warnings, inputEl) {
if (!preSendWarningEl) {
preSendWarningEl = document.createElement('div');
preSendWarningEl.className = 'ss-presend-warning';
document.body.appendChild(preSendWarningEl);
}
const items = warnings.slice(0, 8).map((w, i) => {
const fake = generateFake(w.name, w.value);
const displayVal = w.value.length > 25 ? w.value.slice(0, 22) + '...' : w.value;
return `<div class="ss-ps-item">
<span class="ss-ps-type">${w.name}</span>
<code class="ss-ps-value">${displayVal}</code>
<span class="ss-ps-hint">${w.hint}</span>
${settings.autoAddDetected !== false
? `<button class="ss-ps-add" data-real="${encodeURIComponent(w.value)}" data-fake="${encodeURIComponent(fake)}" data-cat="${w.category}" title="Add mapping: ${displayVal}${fake}">+</button>`
: ''}
</div>`;
}).join('');
const more = warnings.length > 8 ? `<div class="ss-ad-more">+${warnings.length - 8} more</div>` : '';
preSendWarningEl.innerHTML = `
<div class="ss-ad-header">
<strong>Potential PPI detected — not yet configured:</strong>
<button class="ss-ad-close">&times;</button>
</div>
${items}
${more}
<div class="ss-ad-footer">
${settings.autoAddDetected !== false ? 'Click + to auto-add a mapping.' : ''}
Add to Identity or Mappings to protect this data.
</div>
`;
preSendWarningEl.classList.add('visible');
// Close button
preSendWarningEl.querySelector('.ss-ad-close').addEventListener('click', () => {
preSendWarningEl.classList.remove('visible');
});
// Auto-add buttons
preSendWarningEl.querySelectorAll('.ss-ps-add').forEach(btn => {
btn.addEventListener('click', async () => {
const real = decodeURIComponent(btn.dataset.real);
const fake = decodeURIComponent(btn.dataset.fake);
const cat = btn.dataset.cat || 'general';
// Add to mappings via storage
const result = await getStorageData('ss_mappings');
const currentMappings = result || [];
currentMappings.push({
id: crypto.randomUUID(),
real, substitute: fake,
category: cat,
caseSensitive: false,
enabled: true,
createdAt: Date.now(),
});
await setStorageData('ss_mappings', currentMappings);
// Update local mappings
mappings = currentMappings;
// Visual feedback
btn.textContent = '\u2714';
btn.style.color = '#4ade80';
btn.disabled = true;
});
});
}
// Storage helpers for page world (uses postMessage to injector)
function getStorageData(key) {
return new Promise(resolve => {
const id = 'ss-get-' + Math.random();
const handler = (event) => {
if (event.data?.type === 'ss:storage-result' && event.data.id === id) {
window.removeEventListener('message', handler);
resolve(event.data.value);
}
};
window.addEventListener('message', handler);
window.postMessage({ type: 'ss:storage-get', key, id }, '*');
// Timeout fallback
setTimeout(() => { window.removeEventListener('message', handler); resolve(null); }, 2000);
});
}
function setStorageData(key, value) {
window.postMessage({ type: 'ss:storage-set', key, value }, '*');
}
// Scan input on type and paste
let inputScanTimer = null;
function scanInputForPPI(target) {
const text = target.textContent || target.value || '';
if (!text || text.length < 5) {
if (preSendWarningEl) preSendWarningEl.classList.remove('visible');
return;
}
const warnings = autoDetectPPI(text, identity);
if (warnings.length > 0) {
showPreSendWarning(warnings, target);
} else if (preSendWarningEl) {
preSendWarningEl.classList.remove('visible');
}
}
document.addEventListener('input', (e) => {
if (!settings.showHighlights || !hasSubstitutions()) return;
if (settings.autoDetect === false) return;
const target = e.target;
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
const text = target.textContent || target.value || '';
const r = substituteAll(text);
target.classList.toggle('ss-has-sensitive', r.modified);
// Debounce — don't scan on every keystroke
if (inputScanTimer) clearTimeout(inputScanTimer);
inputScanTimer = setTimeout(() => scanInputForPPI(target), 800);
}
}, true);
document.addEventListener('paste', (e) => {
if (settings.autoDetect === false) return;
const target = e.target;
if (target.matches?.('[contenteditable], textarea, input[type="text"]') ||
target.closest?.('[contenteditable]')) {
// Scan shortly after paste completes
setTimeout(() => scanInputForPPI(target.closest?.('[contenteditable]') || target), 200);
}
}, true);
+18
View File
@@ -126,6 +126,24 @@
}, '*');
}
});
// Storage bridge — lets page world script read/write storage
window.addEventListener('message', async (event) => {
if (event.source !== window) return;
if (event.data?.type === 'ss:storage-get') {
const result = await api.storage.local.get(event.data.key);
window.postMessage({
type: 'ss:storage-result',
id: event.data.id,
value: result[event.data.key] || null,
}, '*');
}
if (event.data?.type === 'ss:storage-set') {
await api.storage.local.set({ [event.data.key]: event.data.value });
}
});
}
init();
+1
View File
@@ -20,6 +20,7 @@ const DEFAULT_SETTINGS = {
revealMode: false,
secretScanning: true,
autoDetect: true,
autoAddDetected: true,
maxLogEntries: 200,
customDomains: [],
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'general'],
+10
View File
@@ -67,6 +67,16 @@
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<div>
<label>Offer to auto-add detected PPI</label>
<p class="setting-desc">Show a + button on detected PPI to instantly create a mapping with a suggested fake value</p>
</div>
<label class="toggle">
<input type="checkbox" id="autoAddDetected" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<div>
<label>Max log entries</label>
+5
View File
@@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', async () => {
$('#showHighlights').checked = settings.showHighlights || false;
$('#secretScanning').checked = settings.secretScanning !== false;
$('#autoDetect').checked = settings.autoDetect !== false;
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
$('#maxLogEntries').value = settings.maxLogEntries || 200;
renderMappings();
@@ -46,6 +47,10 @@ document.addEventListener('DOMContentLoaded', async () => {
await Storage.saveSettings({ autoDetect: e.target.checked });
});
$('#autoAddDetected').addEventListener('change', async (e) => {
await Storage.saveSettings({ autoAddDetected: e.target.checked });
});
$('#maxLogEntries').addEventListener('change', async (e) => {
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 });
});