diff --git a/src/content/content.css b/src/content/content.css
index b457e4b..dd5a5f6 100644
--- a/src/content/content.css
+++ b/src/content/content.css
@@ -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;
diff --git a/src/content/content.js b/src/content/content.js
index 5ab2d35..443b86c 100644
--- a/src/content/content.js
+++ b/src/content/content.js
@@ -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 `
+ ${w.name}
+ ${displayVal}
+ ${w.hint}
+ ${settings.autoAddDetected !== false
+ ? ``
+ : ''}
+
`;
+ }).join('');
+
+ const more = warnings.length > 8 ? `+${warnings.length - 8} more
` : '';
+
+ preSendWarningEl.innerHTML = `
+
+ ${items}
+ ${more}
+
+ `;
+
+ 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);
diff --git a/src/content/injector.js b/src/content/injector.js
index 048e93f..fc11458 100644
--- a/src/content/injector.js
+++ b/src/content/injector.js
@@ -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();
diff --git a/src/lib/storage.js b/src/lib/storage.js
index e9b08de..9b8a2bc 100644
--- a/src/lib/storage.js
+++ b/src/lib/storage.js
@@ -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'],
diff --git a/src/options/options.html b/src/options/options.html
index 91b5f63..502a00a 100644
--- a/src/options/options.html
+++ b/src/options/options.html
@@ -67,6 +67,16 @@
+
+
+
+
Show a + button on detected PPI to instantly create a mapping with a suggested fake value
+
+
+
diff --git a/src/options/options.js b/src/options/options.js
index fad2243..24fafcf 100644
--- a/src/options/options.js
+++ b/src/options/options.js
@@ -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 });
});