merge: integrate claude/read-repo-YMu21 branch

Merged changes from the other session:
- Ko-fi support link in options footer
- PPI → PII renaming across codebase
- Legal/licensing updates (stronger disclaimer, non-commercial only)
- Custom secret/auto-redact patterns
- Internal refactoring of secret scanner to auto-redact naming
- Custom domains in popup Options tab
- Additional AI services (Perplexity, DeepSeek, HuggingChat, Poe)
- Developer sites (GitHub, GitLab, Reddit, StackOverflow, Pastebin)
- Dynamic content script registration for custom domains

Resolved conflicts in options.html and popup.html (kept newer
"Auto Redact" naming with custom patterns description).

https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
Claude
2026-03-28 16:39:27 +00:00
14 changed files with 372 additions and 136 deletions
+2 -2
View File
@@ -35,7 +35,7 @@
border-radius: 2px;
}
/* Auto-detect PPI warning banner */
/* Auto-detect PII warning banner */
.ss-autodetect-warning {
position: fixed;
top: 16px;
@@ -136,7 +136,7 @@
font-style: italic;
}
/* Pre-send PPI warning (spellcheck-style, appears while typing) */
/* Pre-send PII warning (spellcheck-style, appears while typing) */
.ss-presend-warning {
position: fixed;
top: 16px;
+47 -36
View File
@@ -287,8 +287,8 @@
}
// ============================================================
// Combined substitution: smart patterns + explicit + secret scan
// + auto-detect warning for unconfigured PPI
// Combined substitution: smart patterns + explicit + auto-redact
// + auto-detect warning for unconfigured PII
// ============================================================
function substituteAll(text) {
const allReplacements = [];
@@ -301,20 +301,20 @@
const explicit = substitute(smart.text, mappings);
allReplacements.push(...explicit.replacements);
// 3. Secret scanner (API keys, tokens, SSNs, credit cards, etc.)
// 3. Auto Redact (API keys, tokens, SSNs, credit cards, custom patterns, etc.)
let finalText = explicit.text;
if (settings.secretScanning !== false) {
const secrets = scanAndRedactSecrets(finalText);
allReplacements.push(...secrets.redactions);
finalText = secrets.text;
if (settings.autoRedact !== false) {
const redacted = runAutoRedact(finalText);
allReplacements.push(...redacted.redactions);
finalText = redacted.text;
}
// 4. Auto-detect: scan the FINAL text for unconfigured PPI
// 4. Auto-detect: scan the FINAL text for unconfigured PII
// Auto-redact if enabled, otherwise just warn
if (settings.autoDetect !== false) {
const warnings = autoDetectPPI(finalText, identity);
const warnings = autoDetectPII(finalText, identity);
if (warnings.length > 0) {
// Auto-redact detected PPI in the outbound text
// Auto-redact detected PII in the outbound text
if (settings.autoRedactDetected !== false) {
for (let i = warnings.length - 1; i >= 0; i--) {
const w = warnings[i];
@@ -343,9 +343,9 @@
}
// ============================================================
// Auto-Detect PPI Scanner (inline for page world)
// Auto-Detect PII Scanner (inline for page world)
// ============================================================
const PPI_PATTERNS = [
const PII_PATTERNS = [
// Network
{ name: 'Private IP', re: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g,
hint: 'Private IP address', cat: 'network' },
@@ -574,7 +574,7 @@
});
}
function autoDetectPPI(text, ident) {
function autoDetectPII(text, ident) {
if (!text || text.length < 5) return [];
const hasContext = CONTEXT_WORDS_RE.test(text);
@@ -595,7 +595,7 @@
}
const findings = [];
for (const pat of PPI_PATTERNS) {
for (const pat of PII_PATTERNS) {
if (pat.contextRequired && !hasContext) continue;
pat.re.lastIndex = 0;
let m;
@@ -649,7 +649,7 @@
safeHTML(warningEl, `
<div class="ss-ad-header">
<strong>Silent Send detected potential PPI that may not be substituted:</strong>
<strong>Silent Send detected potential PII that may not be substituted:</strong>
<button class="ss-ad-close">&times;</button>
</div>
${items}
@@ -672,10 +672,11 @@
}
// ============================================================
// Secret Scanner (inline for page world)
// Detects API keys, tokens, passwords, SSNs, credit cards, etc.
// Auto Redact (inline for page world)
// Detects API keys, tokens, passwords, SSNs, credit cards,
// plus user-defined custom patterns from settings.
// ============================================================
const SECRET_PATTERNS = [
const REDACT_PATTERNS = [
// OpenAI
{ name: 'OpenAI Key', re: /\bsk-[A-Za-z0-9]{20,}\b/g, to: '[REDACTED-OPENAI-KEY]' },
{ name: 'OpenAI Project Key', re: /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g, to: '[REDACTED-OPENAI-KEY]' },
@@ -713,11 +714,21 @@
{ name: 'Credit Card', re: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, to: '[REDACTED-CARD]' },
];
function scanAndRedactSecrets(text) {
function runAutoRedact(text) {
const redactions = [];
let result = text;
for (const pat of SECRET_PATTERNS) {
// Combine built-in + custom patterns
const allPatterns = [...REDACT_PATTERNS];
const custom = settings.customRedactPatterns || [];
for (const cp of custom) {
if (!cp.enabled || !cp.pattern) continue;
try {
allPatterns.push({ name: cp.name, re: new RegExp(cp.pattern, 'g'), to: cp.redact });
} catch { /* invalid regex — skip */ }
}
for (const pat of allPatterns) {
pat.re.lastIndex = 0;
const matches = [];
let m;
@@ -735,7 +746,7 @@
redactions.push({
original: match.value.slice(0, 8) + '...', // Don't log the full secret
replaced: replacement,
category: 'secret',
category: 'redact',
pattern: pat.name,
});
result =
@@ -1036,7 +1047,7 @@
// ============================================================
// Document Upload Processing
//
// Scans files in FormData uploads for PPI. Supports PDF, DOCX,
// Scans files in FormData uploads for PII. Supports PDF, DOCX,
// XLSX, and text files. Shows preview for binary formats.
// ============================================================
@@ -1108,8 +1119,8 @@
}
/**
* Scan a document file for PPI. Strategy: extract text from any
* format, substitute PPI, upload as plaintext. The AI extracts text
* Scan a document file for PII. Strategy: extract text from any
* format, substitute PII, upload as plaintext. The AI extracts text
* from files anyway — no need to preserve formatting in a file
* the user never gets back. Original stays untouched on disk.
*
@@ -1366,7 +1377,7 @@
safeHTML(docPreviewEl, `
<div class="ss-dp-header">
<strong>PPI found in ${esc(filename)}</strong>
<strong>PII found in ${esc(filename)}</strong>
<span class="ss-dp-count">${preview.replacementCount} item(s)</span>
</div>
<div class="ss-dp-note">${preview.note || ''}</div>
@@ -1495,7 +1506,7 @@
}
}
// Also add auto-detect and secret scanner substitutions from this session
// Also add auto-detect and auto-redact substitutions from this session
for (const [key, entry] of sessionSubstitutions) {
if (!pairs.some(p => p.from.toLowerCase() === key)) {
pairs.push({ from: entry.replaced, to: entry.original });
@@ -1815,7 +1826,7 @@
}
// ============================================================
// Pre-Send PPI Detection — scans as you type/paste (spellcheck style)
// Pre-Send PII Detection — scans as you type/paste (spellcheck style)
// ============================================================
// Generate obviously-fake values using reserved/standard ranges
@@ -1879,7 +1890,7 @@
safeHTML(preSendWarningEl, `
<div class="ss-ad-header">
<strong>Potential PPI detected — not yet configured:</strong>
<strong>Potential PII detected — not yet configured:</strong>
<button class="ss-ad-close">&times;</button>
</div>
${items}
@@ -1918,12 +1929,12 @@
// Persist via storage bridge (handles encryption transparently)
setStorageData('ss_mappings', mappings);
// Replace the PPI value in the current input right now
// Replace the PII value in the current input right now
if (inputEl) {
replaceInInput(inputEl, real, fake);
// Re-scan — will dismiss warning if no more PPI remains
// Re-scan — will dismiss warning if no more PII remains
if (inputScanTimer) clearTimeout(inputScanTimer);
inputScanTimer = setTimeout(() => scanInputForPPI(inputEl), 150);
inputScanTimer = setTimeout(() => scanInputForPII(inputEl), 150);
}
// Visual feedback
@@ -1946,7 +1957,7 @@
// Re-scan to update warning
if (inputEl) {
if (inputScanTimer) clearTimeout(inputScanTimer);
inputScanTimer = setTimeout(() => scanInputForPPI(inputEl), 150);
inputScanTimer = setTimeout(() => scanInputForPII(inputEl), 150);
}
});
});
@@ -1997,14 +2008,14 @@
// Scan input on type and paste
let inputScanTimer = null;
function scanInputForPPI(target) {
function scanInputForPII(target) {
const text = target.textContent || target.value || '';
if (!text || text.length < 5) {
if (preSendWarningEl) preSendWarningEl.classList.remove('visible');
return;
}
const warnings = autoDetectPPI(text, identity);
const warnings = autoDetectPII(text, identity);
if (warnings.length > 0) {
showPreSendWarning(warnings, target);
} else if (preSendWarningEl) {
@@ -2018,7 +2029,7 @@
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
// Debounce — don't scan on every keystroke
if (inputScanTimer) clearTimeout(inputScanTimer);
inputScanTimer = setTimeout(() => scanInputForPPI(target), 800);
inputScanTimer = setTimeout(() => scanInputForPII(target), 800);
}
}, true);
@@ -2028,7 +2039,7 @@
if (target.matches?.('[contenteditable], textarea, input[type="text"]') ||
target.closest?.('[contenteditable]')) {
// Scan shortly after paste completes
setTimeout(() => scanInputForPPI(target.closest?.('[contenteditable]') || target), 200);
setTimeout(() => scanInputForPII(target.closest?.('[contenteditable]') || target), 200);
}
}, true);
+7 -7
View File
@@ -1,14 +1,14 @@
/**
* Silent Send - Auto-Detect
*
* Scans text for potential PPI that the user hasn't configured.
* This catches things the identity and secret scanner can't —
* Scans text for potential PII that the user hasn't configured.
* This catches things the identity and auto-redact scanner can't —
* because the user forgot or didn't know to configure them.
*
* Returns warnings (not auto-redactions) so the user can decide.
*/
const PPI_PATTERNS = [
const PII_PATTERNS = [
// --- Network ---
{
name: 'Private IP Address',
@@ -21,7 +21,7 @@ const PPI_PATTERNS = [
regex: /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g,
category: 'network',
hint: 'IP address — could identify your network',
// Exclude common non-PPI IPs
// Exclude common non-PII IPs
exclude: /^(?:127\.0\.0\.1|0\.0\.0\.0|255\.255\.255\.\d+|8\.8\.[84]\.[84]|1\.1\.1\.1|1\.0\.0\.1)$/,
},
{
@@ -126,12 +126,12 @@ const PPI_PATTERNS = [
},
];
// Context words that make ambiguous patterns more likely to be PPI
// Context words that make ambiguous patterns more likely to be PII
const CONTEXT_WORDS = /\b(?:born|birthday|dob|birth|passport|license|driver|ssn|social\s*security|address|home|live|lives|reside|zip|postal)\b/i;
const AutoDetect = {
/**
* Scan text for potential unconfigured PPI.
* Scan text for potential unconfigured PII.
* Pass in identity so we can skip values the user already configured.
*
* Returns array of { name, value, hint, category, index }
@@ -167,7 +167,7 @@ const AutoDetect = {
}
}
for (const pattern of PPI_PATTERNS) {
for (const pattern of PII_PATTERNS) {
// Skip context-dependent patterns if no context words present
if (pattern.contextRequired && !hasContext) continue;
@@ -1,11 +1,13 @@
/**
* Silent Send - Secret Scanner
* Silent Send - Auto Redact
*
* Detects common secret/credential patterns in text and either
* warns or auto-redacts them. This catches things the identity-based
* smart patterns can't: API keys, tokens, passwords, SSNs, credit
* cards, private keys, connection strings, etc.
*
* Supports user-defined custom patterns for proprietary token formats.
*
* Each pattern has:
* - name: human-readable label
* - regex: detection pattern
@@ -13,7 +15,7 @@
* - severity: 'critical' (always redact) or 'warning' (flag but allow)
*/
const SECRET_PATTERNS = [
const REDACT_PATTERNS = [
// --- API Keys ---
{
name: 'OpenAI API Key',
@@ -161,14 +163,39 @@ const SECRET_PATTERNS = [
},
];
const SecretScanner = {
const AutoRedact = {
/**
* Build the full pattern list (built-in + custom).
* Custom patterns come from settings.customRedactPatterns.
*/
_buildPatterns(customPatterns) {
const all = [...REDACT_PATTERNS];
if (Array.isArray(customPatterns)) {
for (const cp of customPatterns) {
if (!cp.enabled || !cp.pattern) continue;
try {
all.push({
name: cp.name || 'Custom Pattern',
regex: new RegExp(cp.pattern, 'g'),
redact: cp.redact || '[REDACTED-CUSTOM]',
severity: 'critical',
});
} catch { /* invalid regex — skip */ }
}
}
return all;
},
/**
* Scan text for secrets. Returns list of findings.
* @param {string} text
* @param {Array} [customPatterns] from settings.customRedactPatterns
*/
scan(text) {
scan(text, customPatterns) {
const findings = [];
const patterns = this._buildPatterns(customPatterns);
for (const pattern of SECRET_PATTERNS) {
for (const pattern of patterns) {
// Reset regex lastIndex
pattern.regex.lastIndex = 0;
let match;
@@ -204,9 +231,11 @@ const SecretScanner = {
/**
* Redact all critical secrets in text. Warnings are not auto-redacted.
* Returns { text, redactions[] }
* @param {string} text
* @param {Array} [customPatterns] from settings.customRedactPatterns
*/
redact(text) {
const findings = this.scan(text);
redact(text, customPatterns) {
const findings = this.scan(text, customPatterns);
const redactions = [];
let result = text;
@@ -220,7 +249,7 @@ const SecretScanner = {
redactions.push({
original: f.value,
replaced: f.redactTo,
category: 'secret',
category: 'redact',
pattern: f.name,
});
}
@@ -237,7 +266,7 @@ const SecretScanner = {
};
if (typeof globalThis !== 'undefined') {
globalThis.SecretScanner = SecretScanner;
globalThis.AutoRedact = AutoRedact;
}
export default SecretScanner;
export default AutoRedact;
+5 -5
View File
@@ -1,11 +1,11 @@
/**
* Silent Send - Document Scanner
*
* Scans uploaded documents for PPI and substitutes/redacts before
* Scans uploaded documents for PII and substitutes/redacts before
* the file reaches the AI service.
*
* Supported formats:
* - PDF: Extract text, scan for PPI, create sanitized plaintext version
* - PDF: Extract text, scan for PII, create sanitized plaintext version
* (PDFs can't be reliably edited in-place without breaking layout)
* - DOCX: Parse XML, find-replace text, repackage ZIP (layout preserved)
* - XLSX: Parse cells, find-replace values, repackage (formatting preserved)
@@ -13,7 +13,7 @@
*
* Modes:
* - Silent: substitute and upload (default for text files)
* - Preview: show PPI findings, let user confirm before upload (default for PDF/DOCX/XLSX)
* - Preview: show PII findings, let user confirm before upload (default for PDF/DOCX/XLSX)
*
* Integration:
* - The fetch interceptor in content.js detects multipart/form-data uploads
@@ -70,9 +70,9 @@ const DocumentScanner = {
},
/**
* PDF: extract text, scan for PPI, create sanitized text file.
* PDF: extract text, scan for PII, create sanitized text file.
* PDFs can't be reliably edited in-place, so we extract text,
* substitute PPI, and send the clean text instead.
* substitute PII, and send the clean text instead.
*/
async _processPDF(file, filename, substituteAll, options) {
const text = await this._extractPDFText(file);
+5 -5
View File
@@ -12,7 +12,7 @@
* - Policy updates are applied automatically
*
* Privacy: the org admin can check compliance (are required fields
* configured?) but CANNOT see individual PPI values.
* configured?) but CANNOT see individual PII values.
*/
import api from './browser-polyfill.js';
@@ -198,11 +198,11 @@ const OrgPolicy = {
},
/**
* Get org-required secret scanner patterns.
* Get org-required auto-redact patterns.
*
* @returns {Array} additional patterns to add to the secret scanner
* @returns {Array} additional patterns to add to auto-redact
*/
async getOrgSecretPatterns() {
async getOrgRedactPatterns() {
const policy = await this.getPolicy();
if (!policy?.requiredSecretPatterns?.length) return [];
@@ -220,7 +220,7 @@ const OrgPolicy = {
/**
* Check if the user's configuration meets org policy requirements.
* Returns compliance status WITHOUT revealing actual PPI values.
* Returns compliance status WITHOUT revealing actual PII values.
*
* @returns {{ compliant: boolean, missing: string[], configured: string[] }}
*/
+3 -2
View File
@@ -23,7 +23,7 @@ const KEYS = {
SETTINGS: 'ss_settings',
};
// Keys that contain sensitive PPI and should be encrypted at rest
// Keys that contain sensitive PII and should be encrypted at rest
// All user data keys are encrypted at rest — settings included since
// custom domains and configuration can reveal what services the user
// accesses. Only ss_sync_encryption (salt, verification blob) and
@@ -34,12 +34,13 @@ const DEFAULT_SETTINGS = {
enabled: true,
showHighlights: false,
revealMode: false,
secretScanning: true,
autoRedact: true,
autoDetect: true,
autoRedactDetected: true,
autoAddDetected: true,
maxLogEntries: 100,
customDomains: [],
customRedactPatterns: [],
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
browserSync: false,
};
+37 -9
View File
@@ -49,17 +49,38 @@
</div>
<div class="setting-row">
<div>
<label>Auto-redact secrets</label>
<p class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit card numbers</p>
<label>Auto Redact</label>
<p class="setting-desc">Automatically detect and redact API keys, tokens, passwords, SSNs, credit card numbers, and custom patterns</p>
</div>
<label class="toggle">
<input type="checkbox" id="secretScanning" checked>
<input type="checkbox" id="autoRedactToggle" checked>
<span class="toggle-slider"></span>
</label>
</div>
<!-- Custom Secret Patterns -->
<div style="margin:12px 0;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
<h3 style="font-size:13px;font-weight:600;margin:0 0 4px">Custom Redact Patterns</h3>
<p class="setting-desc" style="margin-bottom:8px">
Define your own patterns to catch proprietary tokens, internal URLs with keys, or any format the built-in scanner doesn't cover.
</p>
<div id="customRedactList"></div>
<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">
<div style="display:flex;gap:6px;flex-wrap:wrap">
<input type="text" id="newRedactName" placeholder="Name (e.g. ControlD Token)" style="flex:1;min-width:140px;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
<input type="text" id="newRedactPattern" placeholder="Regex or prefix (e.g. ctrl_[A-Za-z0-9]{20,})" style="flex:2;min-width:200px;font-size:12px;font-family:monospace;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
</div>
<div style="display:flex;gap:6px;align-items:center">
<input type="text" id="newRedactReplacement" placeholder="Replacement (default: [REDACTED-NAME])" style="flex:1;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
<button class="btn btn-primary btn-sm" id="btnAddRedactPattern">Add Pattern</button>
</div>
</div>
<p class="setting-desc" style="margin-top:6px;margin-bottom:0">
<strong>Tip:</strong> For a URL like <code>https://dns.example.com/abc123</code>, use a pattern like <code>dns\.example\.com/[A-Za-z0-9;]+</code> to match the secret path segment.
</p>
</div>
<div class="setting-row">
<div>
<label>Auto-detect unconfigured PPI</label>
<p class="setting-desc">Warn when potential personal data (IPs, addresses, paths) is detected that you haven't configured</p>
</div>
<label class="toggle">
@@ -69,8 +90,8 @@
</div>
<div class="setting-row">
<div>
<label>Auto-redact detected PPI on send</label>
<p class="setting-desc">Automatically replace detected PPI with generic placeholders (192.0.2.1, 123 Example Street, etc.) when sending</p>
<label>Auto-redact detected PII on send</label>
<p class="setting-desc">Automatically replace detected PII with generic placeholders (192.0.2.1, 123 Example Street, etc.) when sending</p>
</div>
<label class="toggle">
<input type="checkbox" id="autoRedactDetected" checked>
@@ -79,8 +100,8 @@
</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>
<label>Offer to auto-add detected PII</label>
<p class="setting-desc">Show a + button on detected PII to instantly create a mapping with a suggested fake value</p>
</div>
<label class="toggle">
<input type="checkbox" id="autoAddDetected" checked>
@@ -611,6 +632,13 @@
<footer>
<p>Silent Send v0.9.0</p>
<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.
</p>
<p style="font-size:11px;color:#9ca3af;margin-top:8px">
Find Silent Send useful? No obligation, but if you'd like to help keep it going:
<a href="https://ko-fi.com/YOUR_KOFI_USERNAME" target="_blank" style="color:#6b7280">Buy me a coffee</a>
</p>
</footer>
</div>
+94 -3
View File
@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// Apply settings to UI
$('#showHighlights').checked = settings.showHighlights || false;
$('#secretScanning').checked = settings.secretScanning !== false;
$('#autoRedactToggle').checked = settings.autoRedact !== false;
$('#autoDetect').checked = settings.autoDetect !== false;
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
@@ -299,8 +299,15 @@ document.addEventListener('DOMContentLoaded', async () => {
await Storage.saveSettings({ showHighlights: e.target.checked });
});
$('#secretScanning').addEventListener('change', async (e) => {
await Storage.saveSettings({ secretScanning: e.target.checked });
$('#autoRedactToggle').addEventListener('change', async (e) => {
await Storage.saveSettings({ autoRedact: e.target.checked });
});
// Custom redact patterns
renderCustomRedactPatterns();
$('#btnAddRedactPattern').addEventListener('click', addCustomRedactPattern);
$('#newRedactPattern').addEventListener('keydown', (e) => {
if (e.key === 'Enter') addCustomRedactPattern();
});
$('#autoDetect').addEventListener('change', async (e) => {
@@ -804,6 +811,90 @@ function renderDomains() {
});
}
// --- Custom Redact Patterns ---
function addCustomRedactPattern() {
const name = $('#newRedactName').value.trim();
const pattern = $('#newRedactPattern').value.trim();
const redact = $('#newRedactReplacement').value.trim();
if (!pattern) { alert('Pattern is required.'); return; }
// Validate regex
try {
new RegExp(pattern, 'g');
} catch (e) {
alert('Invalid regex: ' + e.message);
return;
}
const label = name || 'Custom Pattern';
const replacement = redact || `[REDACTED-${label.toUpperCase().replace(/\s+/g, '-')}]`;
const patterns = settings.customRedactPatterns || [];
patterns.push({
id: crypto.randomUUID(),
name: label,
pattern,
redact: replacement,
enabled: true,
});
settings.customRedactPatterns = patterns;
Storage.saveSettings({ customRedactPatterns: patterns });
renderCustomRedactPatterns();
$('#newRedactName').value = '';
$('#newRedactPattern').value = '';
$('#newRedactReplacement').value = '';
}
function renderCustomRedactPatterns() {
const list = $('#customRedactList');
if (!list) return;
const patterns = settings.customRedactPatterns || [];
if (patterns.length === 0) {
safeHTML(list, '<div style="font-size:12px;color:#9ca3af;padding:6px 0">No custom patterns defined. Built-in patterns cover common API keys, tokens, and credentials.</div>');
return;
}
safeHTML(list, patterns.map((p, i) => `
<div style="display:flex;align-items:center;gap:6px;padding:6px 8px;background:#fff;border:1px solid #e5e7eb;border-radius:6px;margin-bottom:4px;flex-wrap:wrap">
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;min-width:0">
<input type="checkbox" class="redact-toggle" data-index="${i}" ${p.enabled ? 'checked' : ''}>
</label>
<span style="font-size:12px;font-weight:500;white-space:nowrap">${escapeHtml(p.name)}</span>
<code style="font-size:11px;color:#6b7280;background:#f3f4f6;padding:1px 5px;border-radius:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:250px" title="${escapeHtml(p.pattern)}">${escapeHtml(p.pattern)}</code>
<span style="font-size:11px;color:#9ca3af;margin-left:auto;white-space:nowrap">&rarr; ${escapeHtml(p.redact)}</span>
<button class="btn btn-sm btn-danger btn-remove-redact" data-index="${i}" style="padding:2px 6px">&times;</button>
</div>
`).join(''));
// Toggle handlers
list.querySelectorAll('.redact-toggle').forEach(toggle => {
toggle.addEventListener('change', async () => {
const idx = parseInt(toggle.dataset.index, 10);
const patterns = settings.customRedactPatterns || [];
patterns[idx].enabled = toggle.checked;
settings.customRedactPatterns = patterns;
await Storage.saveSettings({ customRedactPatterns: patterns });
});
});
// Remove handlers
list.querySelectorAll('.btn-remove-redact').forEach(btn => {
btn.addEventListener('click', async () => {
const idx = parseInt(btn.dataset.index, 10);
const patterns = settings.customRedactPatterns || [];
patterns.splice(idx, 1);
settings.customRedactPatterns = patterns;
await Storage.saveSettings({ customRedactPatterns: patterns });
renderCustomRedactPatterns();
});
});
}
// --- Transfer Data (Export/Import All) ---
async function getAllData() {
+14 -10
View File
@@ -198,15 +198,15 @@
<section class="tab-content" id="tab-options">
<div class="setting-item">
<div class="setting-label">
<strong>Auto-redact secrets</strong>
<span class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit cards</span>
<strong>Auto Redact</strong>
<span class="setting-desc">Automatically redact API keys, tokens, passwords, SSNs, credit cards, and custom patterns</span>
</div>
<label class="toggle"><input type="checkbox" id="optSecretScanning" checked><span class="toggle-slider"></span></label>
<label class="toggle"><input type="checkbox" id="optAutoRedact" checked><span class="toggle-slider"></span></label>
</div>
<div class="setting-item">
<div class="setting-label">
<strong>Auto-detect PPI</strong>
<strong>Auto-detect PII</strong>
<span class="setting-desc">Warn about unconfigured personal data (IPs, addresses, paths)</span>
</div>
<label class="toggle"><input type="checkbox" id="optAutoDetect" checked><span class="toggle-slider"></span></label>
@@ -214,8 +214,8 @@
<div class="setting-item">
<div class="setting-label">
<strong>Auto-redact detected PPI</strong>
<span class="setting-desc">Replace detected PPI with placeholders on send</span>
<strong>Auto-redact detected PII</strong>
<span class="setting-desc">Replace detected PII with placeholders on send</span>
</div>
<label class="toggle"><input type="checkbox" id="optAutoRedact" checked><span class="toggle-slider"></span></label>
</div>
@@ -231,7 +231,7 @@
<div class="setting-item">
<div class="setting-label">
<strong>Document scan preview</strong>
<span class="setting-desc">Show PPI findings before uploading documents</span>
<span class="setting-desc">Show PII findings before uploading documents</span>
</div>
<label class="toggle"><input type="checkbox" id="optDocPreview" checked><span class="toggle-slider"></span></label>
</div>
@@ -271,9 +271,13 @@
AES-256 encrypted at rest — unreadable without your password.</span>
</div>
<div class="privacy-note" style="color:#b45309;background:#fef3c7;padding:6px 8px;border-radius:4px;margin-bottom:6px">
Silent Send is a convenience tool, not a security guarantee. It can miss
PPI in images, file uploads, unusual name forms, or data you forgot to
configure. Always verify sensitive messages before sending.
Silent Send is a convenience tool, not a security guarantee. It reduces
but cannot eliminate the risk of sharing personal data. It may miss PII
in images, unusual formats, or data you haven't configured. Third-party
sites may change how they send data at any time, which can cause missed
substitutions without warning. Always verify sensitive messages before
sending. By using this extension, you accept full responsibility for
verifying your data is protected.
</div>
</footer>
</div>
+22 -22
View File
@@ -1,6 +1,6 @@
import SubstitutionEngine from '../lib/substitution-engine.js';
import SmartPatterns from '../lib/smart-patterns.js';
import SecretScanner from '../lib/secret-scanner.js';
import AutoRedact from '../lib/auto-redact.js';
import AutoDetect from '../lib/auto-detect.js';
import Storage from '../lib/storage.js';
import SilentSendSync from '../lib/sync.js';
@@ -214,7 +214,7 @@ async function initUnlockedUI() {
});
// Load options tab settings
$('#optSecretScanning').checked = settings.secretScanning !== false;
$('#optAutoRedact').checked = settings.autoRedact !== false;
$('#optAutoDetect').checked = settings.autoDetect !== false;
$('#optAutoRedact').checked = settings.autoRedactDetected !== false;
$('#optHighlights').checked = settings.showHighlights || false;
@@ -223,7 +223,7 @@ async function initUnlockedUI() {
// Options tab change handlers
const optHandlers = [
['optSecretScanning', 'secretScanning'],
['optAutoRedact', 'autoRedact'],
['optAutoDetect', 'autoDetect'],
['optAutoRedact', 'autoRedactDetected'],
['optHighlights', 'showHighlights'],
@@ -715,16 +715,16 @@ function renderTestDiff() {
const smartResult = SmartPatterns.substitute(input, identity);
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
const secretResult = SecretScanner.redact(explicitResult.text);
const redactResult = AutoRedact.redact(explicitResult.text, settings.customRedactPatterns);
const allReplacements = [
...smartResult.replacements,
...explicitResult.replacements,
...secretResult.redactions,
...redactResult.redactions,
];
const finalText = secretResult.text;
const finalText = redactResult.text;
if (finalText === input && secretResult.warnings.length === 0) {
if (finalText === input && redactResult.warnings.length === 0) {
output.textContent = input;
stats.textContent = 'No substitutions detected';
return;
@@ -740,8 +740,8 @@ function renderTestDiff() {
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
);
}
// Highlight secret redactions in red
for (const r of secretResult.redactions) {
// Highlight auto-redactions in red
for (const r of redactResult.redactions) {
const escapedReplaced = escapeHtml(r.replaced);
html = html.replace(
escapedReplaced,
@@ -752,28 +752,28 @@ function renderTestDiff() {
const smartCount = smartResult.replacements.length;
const explicitCount = explicitResult.replacements.length;
const secretCount = secretResult.redactions.length;
const warnCount = secretResult.warnings.length;
const redactCount = redactResult.redactions.length;
const warnCount = redactResult.warnings.length;
const parts = [];
if (smartCount > 0) parts.push(`${smartCount} smart`);
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
if (secretCount > 0) parts.push(`${secretCount} secrets redacted`);
if (redactCount > 0) parts.push(`${redactCount} auto-redacted`);
if (warnCount > 0) parts.push(`${warnCount} warnings`);
// Auto-detect unconfigured PPI in the final text
const ppiWarnings = AutoDetect.scan(finalText, identity);
if (ppiWarnings.length > 0) parts.push(`${ppiWarnings.length} PPI detected`);
// Auto-detect unconfigured PII in the final text
const piiWarnings = AutoDetect.scan(finalText, identity);
if (piiWarnings.length > 0) parts.push(`${piiWarnings.length} PII detected`);
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
// Show PPI warnings below stats
if (ppiWarnings.length > 0) {
const ppiDiv = document.createElement('div');
safeHTML(ppiDiv, `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
<strong>Unconfigured PPI detected:</strong>
${ppiWarnings.map(w => `<div style="margin-top:3px"><code style="background:#fff;padding:1px 4px;border-radius:2px;color:#b45309">${escapeHtml(w.value)}</code> — ${w.hint}</div>`).join('')}
// Show PII warnings below stats
if (piiWarnings.length > 0) {
const piiDiv = document.createElement('div');
safeHTML(piiDiv, `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
<strong>Unconfigured PII detected:</strong>
${piiWarnings.map(w => `<div style="margin-top:3px"><code style="background:#fff;padding:1px 4px;border-radius:2px;color:#b45309">${escapeHtml(w.value)}</code> — ${w.hint}</div>`).join('')}
</div>`);
stats.appendChild(ppiDiv);
stats.appendChild(piiDiv);
}
}