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;
|
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 */
|
/* Floating reveal mode indicator */
|
||||||
.ss-reveal-badge {
|
.ss-reveal-badge {
|
||||||
position: fixed;
|
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) => {
|
document.addEventListener('input', (e) => {
|
||||||
if (!settings.showHighlights || !hasSubstitutions()) return;
|
if (settings.autoDetect === false) return;
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
|
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
|
||||||
const text = target.textContent || target.value || '';
|
// Debounce — don't scan on every keystroke
|
||||||
const r = substituteAll(text);
|
if (inputScanTimer) clearTimeout(inputScanTimer);
|
||||||
target.classList.toggle('ss-has-sensitive', r.modified);
|
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);
|
}, 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();
|
init();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
revealMode: false,
|
revealMode: false,
|
||||||
secretScanning: true,
|
secretScanning: true,
|
||||||
autoDetect: true,
|
autoDetect: true,
|
||||||
|
autoAddDetected: true,
|
||||||
maxLogEntries: 200,
|
maxLogEntries: 200,
|
||||||
customDomains: [],
|
customDomains: [],
|
||||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'general'],
|
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'general'],
|
||||||
|
|||||||
@@ -67,6 +67,16 @@
|
|||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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 class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
<label>Max log entries</label>
|
<label>Max log entries</label>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
$('#showHighlights').checked = settings.showHighlights || false;
|
$('#showHighlights').checked = settings.showHighlights || false;
|
||||||
$('#secretScanning').checked = settings.secretScanning !== false;
|
$('#secretScanning').checked = settings.secretScanning !== false;
|
||||||
$('#autoDetect').checked = settings.autoDetect !== false;
|
$('#autoDetect').checked = settings.autoDetect !== false;
|
||||||
|
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
|
||||||
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
||||||
|
|
||||||
renderMappings();
|
renderMappings();
|
||||||
@@ -46,6 +47,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
await Storage.saveSettings({ autoDetect: e.target.checked });
|
await Storage.saveSettings({ autoDetect: e.target.checked });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#autoAddDetected').addEventListener('change', async (e) => {
|
||||||
|
await Storage.saveSettings({ autoAddDetected: e.target.checked });
|
||||||
|
});
|
||||||
|
|
||||||
$('#maxLogEntries').addEventListener('change', async (e) => {
|
$('#maxLogEntries').addEventListener('change', async (e) => {
|
||||||
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 });
|
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user