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:
@@ -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
@@ -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">×</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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user