revert: restore entire src/ from pre-doc-scanner commit (e4b44a7)

Going back to a known-good baseline. This version had:
- Working reveal mode with CSS Highlight API
- Working substitution (fetch + XHR hooks)
- Smart patterns (names, emails, phones, usernames)
- Encryption/sync (password, TOTP, WebAuthn)
- Multiple identity profiles
- Activity log
- Secret scanner
- Auto-detect PII warnings
- Pre-send PII detection

Kept current manifests (UUID, data_collection_permissions, version).
No renames applied — uses original naming (secretScanning, PPI, etc).
Will re-apply renames and new features from this working base.

https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
This commit is contained in:
Claude
2026-03-29 18:45:10 +00:00
parent 73bc08dad2
commit 2719e1be44
17 changed files with 302 additions and 1902 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Silent Send",
"version": "0.9.20",
"version": "0.9.21",
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"browser_specific_settings": {
"gecko": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Silent Send",
"version": "0.9.20",
"version": "0.9.21",
"description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"permissions": [
"storage",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "silent-send",
"version": "0.9.20",
"version": "0.9.21",
"private": true,
"license": "MIT",
"description": "Browser extension that substitutes personal data before sending to AI services",
-70
View File
@@ -21,7 +21,6 @@ const tabCounts = new Map();
// Built-in URL patterns
const BUILTIN_URL_PATTERNS = [
// AI services
'https://claude.ai/*',
'https://chatgpt.com/*',
'https://chat.openai.com/*',
@@ -29,19 +28,6 @@ const BUILTIN_URL_PATTERNS = [
'https://grok.x.ai/*',
'https://x.com/i/grok*',
'https://gemini.google.com/*',
'https://www.perplexity.ai/*',
'https://copilot.microsoft.com/*',
'https://chat.deepseek.com/*',
'https://huggingface.co/chat/*',
'https://poe.com/*',
// Developer & support sites
'https://github.com/*',
'https://gitlab.com/*',
'https://www.reddit.com/*',
'https://old.reddit.com/*',
'https://stackoverflow.com/*',
'https://pastebin.com/*',
// Local
'http://localhost/*',
'http://127.0.0.1/*',
];
@@ -76,44 +62,6 @@ api.tabs.onRemoved.addListener((tabId) => {
// --- Dynamic injection for custom domains ---
const DYNAMIC_SCRIPT_ID = 'ss-custom-domains';
/**
* Sync the dynamically registered content script with the current
* custom domains list. Uses scripting.registerContentScripts so custom
* domains inject at document_start (like built-in sites) and persist
* across service worker restarts.
*/
async function syncCustomDomainScripts() {
const settings = await Storage.getSettings();
const customDomains = settings.customDomains || [];
// Build match patterns from domains (e.g. "https://my-ai.com" → "https://my-ai.com/*")
const matches = customDomains.map(d => d.replace(/\/$/, '') + '/*');
try {
// Remove existing dynamic script first
await api.scripting.unregisterContentScripts({ ids: [DYNAMIC_SCRIPT_ID] }).catch(() => {});
if (matches.length > 0) {
await api.scripting.registerContentScripts([{
id: DYNAMIC_SCRIPT_ID,
matches,
js: ['src/content/injector.js'],
css: ['src/content/content.css'],
runAt: 'document_start',
allFrames: true,
}]);
}
} catch (e) {
console.warn('[Silent Send] Failed to register custom domain scripts:', e);
}
}
/**
* Inject on an already-open tab for a newly added custom domain.
* Only needed for tabs that were open before the content script was registered.
*/
async function injectOnCustomDomain(tabId, tabUrl) {
const settings = await Storage.getSettings();
const customDomains = settings.customDomains || [];
@@ -164,17 +112,6 @@ api.runtime.onMessage.addListener((message, sender, sendResponse) => {
});
const messageHandlers = {
async 'get:decrypted-config'(_message, _sender, sendResponse) {
try {
const mappings = await Storage.getMappings();
const identity = await Storage.getIdentity();
const settings = await Storage.getSettings();
sendResponse({ mappings, identity, settings });
} catch {
sendResponse(null);
}
},
async 'substitution:performed'(message, sender) {
const tabId = sender.tab?.id;
if (tabId == null) return;
@@ -437,11 +374,6 @@ api.storage.onChanged.addListener(async (changes, areaName) => {
const settings = await Storage.getSettings();
await updateIcon(settings);
// Re-register dynamic content scripts when custom domains change
if (changes.ss_settings && areaName === 'local') {
await syncCustomDomainScripts();
}
// Push to browser.storage.sync when local data changes (same-browser cross-device)
if (areaName === 'local' && settings.browserSync) {
await SilentSendSync.pushToSyncStorage();
@@ -567,7 +499,6 @@ api.runtime.onInstalled.addListener(async () => {
api.action.setBadgeBackgroundColor({ color: '#6b7280' });
const settings = await Storage.getSettings();
await updateIcon(settings);
await syncCustomDomainScripts();
// Set up alarms on install
await setupAutoSyncAlarm();
await setupOrgPolicyAlarm();
@@ -577,7 +508,6 @@ api.runtime.onInstalled.addListener(async () => {
(async () => {
const settings = await Storage.getSettings();
await updateIcon(settings);
await syncCustomDomainScripts();
// Check if extension is locked (encrypted data, no cached key)
const locked = await Storage.isLocked();
+2 -127
View File
@@ -35,7 +35,7 @@
border-radius: 2px;
}
/* Auto-detect PII warning banner */
/* Auto-detect PPI warning banner */
.ss-autodetect-warning {
position: fixed;
top: 16px;
@@ -136,7 +136,7 @@
font-style: italic;
}
/* Pre-send PII warning (spellcheck-style, appears while typing) */
/* Pre-send PPI warning (spellcheck-style, appears while typing) */
.ss-presend-warning {
position: fixed;
top: 16px;
@@ -220,21 +220,6 @@
.ss-ps-add:hover { background: rgba(74, 222, 128, 0.15); }
.ss-ps-add:disabled { border-color: #333; cursor: default; }
.ss-ps-ignore {
background: #4b5563;
border: none;
color: #d1d5db;
font-size: 9px;
cursor: pointer;
flex-shrink: 0;
padding: 3px 6px;
border-radius: 3px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ss-ps-ignore:hover { color: #e5e7eb; text-decoration: underline; }
/* Floating reveal mode indicator */
.ss-reveal-badge {
position: fixed;
@@ -259,113 +244,3 @@
opacity: 1;
transform: translateY(0);
}
/* Document scan preview overlay */
.ss-doc-preview {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
max-width: 480px;
width: 90vw;
background: #1a1a1a;
color: #e5e7eb;
border: 1px solid #f59e0b;
border-radius: 12px;
padding: 16px 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
z-index: 9999999;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
max-height: 70vh;
overflow-y: auto;
}
.ss-doc-preview.visible {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
pointer-events: auto;
}
.ss-dp-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.ss-dp-count {
font-size: 11px;
color: #f59e0b;
font-weight: 600;
}
.ss-dp-note {
font-size: 11px;
color: #9ca3af;
margin-bottom: 10px;
font-style: italic;
}
.ss-dp-items {
max-height: 200px;
overflow-y: auto;
margin-bottom: 12px;
}
.ss-dp-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 0;
border-bottom: 1px solid #333;
font-size: 11px;
}
.ss-dp-item:last-child { border-bottom: none; }
.ss-dp-orig {
color: #f87171;
background: #1f1f1f;
padding: 2px 5px;
border-radius: 3px;
}
.ss-dp-repl {
color: #4ade80;
background: #1f1f1f;
padding: 2px 5px;
border-radius: 3px;
}
.ss-dp-actions {
display: flex;
gap: 8px;
}
.ss-dp-btn {
flex: 1;
padding: 8px 12px;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
}
.ss-dp-confirm {
background: #10b981;
color: #fff;
}
.ss-dp-confirm:hover { background: #059669; }
.ss-dp-cancel {
background: #374151;
color: #e5e7eb;
}
.ss-dp-cancel:hover { background: #4b5563; }
+125 -787
View File
File diff suppressed because it is too large Load Diff
+10 -45
View File
@@ -57,57 +57,22 @@
// Load mappings and settings, then inject into page
async function init() {
let mappings, identity, settings;
const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']);
const isEncrypted = result.ss_mappings?._ssLocalEncrypted ||
result.ss_identity?._ssLocalEncrypted;
const settings = result.ss_settings || { enabled: true };
if (isEncrypted) {
// Data is encrypted — ask the background script for decrypted config.
// The background has access to the Storage module which can decrypt.
try {
const response = await api.runtime.sendMessage({ type: 'get:decrypted-config' });
if (response?.mappings) {
mappings = response.mappings;
identity = response.identity || {};
settings = response.settings || { enabled: true };
} else {
// Background couldn't decrypt (locked) — inject with empty config
// and wait for vault:unlocked message later
mappings = [];
identity = {};
settings = result.ss_settings || { enabled: true };
}
} catch {
mappings = [];
identity = {};
settings = result.ss_settings || { enabled: true };
}
} else {
// Data is plaintext — read directly
mappings = result.ss_mappings || [];
const identityData = result.ss_identity || {};
identity = mergeProfiles(identityData);
settings = result.ss_settings || { enabled: true };
}
// Check if data is encrypted (locked) — pass empty config
// The background will send decrypted data via vault:unlocked when ready
const isLocked = result.ss_mappings?._ssLocalEncrypted ||
result.ss_identity?._ssLocalEncrypted;
const mappings = isLocked ? [] : (result.ss_mappings || []);
const identityData = isLocked ? {} : (result.ss_identity || {});
// Ensure identity is merged if it came from background
if (identity.profiles) {
identity = mergeProfiles(identity);
}
// Inject config as a global variable before loading content.js
// Using a separate inline-data element ensures content.js can read it
// even if there's a race condition with script.onload removal
const configEl = document.createElement('script');
configEl.type = 'application/json';
configEl.id = 'ss-config-data';
configEl.textContent = JSON.stringify({ mappings, identity, settings });
(document.head || document.documentElement).appendChild(configEl);
// Merge active profiles into a flat identity object for the content script
const identity = mergeProfiles(identityData);
// Inject the main interception script into the page's world
const script = document.createElement('script');
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
script.src = api.runtime.getURL('src/content/content.js');
(document.head || document.documentElement).appendChild(script);
script.onload = () => script.remove();
+15 -93
View File
@@ -1,14 +1,14 @@
/**
* Silent Send - Auto-Detect
*
* Scans text for potential PII that the user hasn't configured.
* This catches things the identity and auto-redact scanner can't —
* Scans text for potential PPI that the user hasn't configured.
* This catches things the identity and secret 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 PII_PATTERNS = [
const PPI_PATTERNS = [
// --- Network ---
{
name: 'Private IP Address',
@@ -21,7 +21,7 @@ const PII_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-PII IPs
// Exclude common non-PPI 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 PII_PATTERNS = [
},
];
// Context words that make ambiguous patterns more likely to be PII
// Context words that make ambiguous patterns more likely to be PPI
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 PII.
* Scan text for potential unconfigured PPI.
* 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 PII_PATTERNS) {
for (const pattern of PPI_PATTERNS) {
// Skip context-dependent patterns if no context words present
if (pattern.contextRequired && !hasContext) continue;
@@ -221,15 +221,16 @@ const AutoDetect = {
*/
_detectProperNouns(text, configured) {
const findings = [];
// Only match TWO OR MORE consecutive capitalized words
// Single capitalized words cause too many false positives (sentence starts)
const re = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})+)\b/g;
const re = /(?:^|[.!?\n]\s*)?([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*)/g;
let m;
while ((m = re.exec(text)) !== null) {
const fullMatch = m[1];
if (!fullMatch) continue;
const before = text.slice(Math.max(0, m.index - 2), m.index);
const isSentenceStart = m.index === 0 || /[.!?\n]\s*$/.test(before);
const words = fullMatch.split(/\s+/);
const properWords = words.filter(w =>
w.length >= 3 &&
@@ -237,14 +238,15 @@ const AutoDetect = {
!configured.has(w.toLowerCase())
);
if (properWords.length < 2) continue; // need at least 2 proper words
if (properWords.length === 0) continue;
if (isSentenceStart && properWords.length === 1 && words.length === 1) continue;
const value = properWords.join(' ');
if (value.length >= 5 && !configured.has(value.toLowerCase())) {
if (value.length >= 3 && !configured.has(value.toLowerCase())) {
findings.push({
name: 'Possible Name/Org',
value,
hint: 'Capitalized phrase — could be a name, company, or project',
hint: 'Capitalized word — could be a name, company, or project',
category: 'name',
});
}
@@ -260,8 +262,6 @@ const AutoDetect = {
};
// Common English words to exclude from proper noun detection
// Comprehensive list including verbs, nouns, adjectives that appear
// in titles, headings, UI buttons, and instructions
const COMMON_WORDS = new Set([
'the', 'and', 'but', 'for', 'not', 'you', 'all', 'can', 'had', 'her',
'was', 'one', 'our', 'out', 'are', 'has', 'his', 'how', 'its', 'may',
@@ -281,84 +281,6 @@ const COMMON_WORDS = new Set([
'number', 'other', 'point', 'right', 'small', 'state', 'thing',
'think', 'those', 'three', 'through', 'under', 'until', 'water',
'world', 'write', 'might', 'should', 'because', 'although',
// Common verbs (titles, headings, buttons, instructions)
'generate', 'design', 'manage', 'process', 'handle', 'check', 'verify',
'submit', 'apply', 'accept', 'reject', 'approve', 'deny', 'confirm',
'cancel', 'delete', 'remove', 'edit', 'modify', 'view', 'display',
'search', 'filter', 'sort', 'select', 'choose', 'pick', 'enter',
'upload', 'download', 'install', 'enable', 'disable', 'activate',
'connect', 'disconnect', 'sync', 'refresh', 'reload', 'reset',
'save', 'load', 'store', 'restore', 'backup', 'copy', 'paste',
'lock', 'unlock', 'encrypt', 'decrypt', 'sign', 'register', 'login',
'logout', 'subscribe', 'share', 'publish', 'deploy', 'launch',
'merge', 'split', 'join', 'link', 'attach', 'insert', 'append',
'format', 'parse', 'convert', 'transform', 'translate', 'compile',
'execute', 'render', 'animate', 'validate', 'sanitize', 'escape',
'create', 'build', 'start', 'stop', 'open', 'close', 'run', 'send',
// Common nouns (titles, headings, labels)
'account', 'action', 'address', 'alert', 'analysis', 'application',
'area', 'article', 'asset', 'background', 'badge', 'banner', 'board',
'body', 'border', 'bottom', 'box', 'browser', 'buffer', 'button',
'cache', 'calendar', 'card', 'category', 'center', 'channel', 'chart',
'chat', 'child', 'choice', 'client', 'cloud', 'code', 'collection',
'color', 'column', 'command', 'comment', 'community', 'company',
'component', 'config', 'configuration', 'connection', 'console',
'contact', 'container', 'content', 'context', 'control', 'count',
'country', 'custom', 'dashboard', 'data', 'database', 'date', 'day',
'default', 'description', 'design', 'desktop', 'detail', 'device',
'dialog', 'directory', 'document', 'domain', 'draft', 'driver',
'edge', 'editor', 'element', 'email', 'engine', 'entry', 'environment',
'error', 'event', 'example', 'extension', 'feature', 'feedback',
'field', 'file', 'filter', 'folder', 'font', 'footer', 'form',
'frame', 'function', 'gallery', 'general', 'global', 'grid',
'guide', 'handler', 'header', 'health', 'help', 'history', 'home',
'host', 'icon', 'image', 'index', 'info', 'input', 'instance',
'interface', 'issue', 'item', 'job', 'key', 'label', 'language',
'layout', 'level', 'library', 'light', 'limit', 'line', 'link',
'list', 'local', 'location', 'log', 'logo', 'main', 'manager',
'manual', 'map', 'media', 'member', 'memory', 'menu', 'message',
'method', 'mobile', 'modal', 'mode', 'model', 'module', 'monitor',
'navigation', 'network', 'node', 'note', 'notification', 'object',
'option', 'order', 'origin', 'output', 'overlay', 'overview', 'owner',
'package', 'page', 'panel', 'parent', 'parser', 'password', 'path',
'pattern', 'permission', 'photo', 'pipeline', 'placeholder', 'plan',
'platform', 'player', 'plugin', 'point', 'policy', 'pool', 'popup',
'port', 'position', 'post', 'power', 'preview', 'primary', 'print',
'priority', 'process', 'product', 'profile', 'program', 'progress',
'project', 'prompt', 'property', 'protocol', 'provider', 'proxy',
'public', 'query', 'queue', 'quick', 'range', 'rate', 'reader',
'record', 'region', 'release', 'remote', 'report', 'request',
'resource', 'response', 'result', 'review', 'role', 'root', 'route',
'row', 'rule', 'runtime', 'sample', 'scanner', 'schema', 'scope',
'screen', 'script', 'search', 'section', 'security', 'select',
'sender', 'server', 'service', 'session', 'setting', 'settings',
'setup', 'share', 'shell', 'shortcut', 'sidebar', 'signal', 'simple',
'single', 'site', 'size', 'slider', 'snapshot', 'socket', 'solution',
'source', 'space', 'stage', 'standard', 'status', 'step', 'storage',
'stream', 'string', 'style', 'subject', 'success', 'summary',
'support', 'switch', 'symbol', 'syntax', 'system', 'table', 'target',
'task', 'team', 'template', 'terminal', 'test', 'text', 'theme',
'thread', 'title', 'token', 'tool', 'toolbar', 'tooltip', 'total',
'track', 'traffic', 'tree', 'trigger', 'type', 'unit', 'update',
'upload', 'user', 'utility', 'value', 'variable', 'version', 'video',
'view', 'virtual', 'warning', 'watch', 'web', 'widget', 'width',
'window', 'wizard', 'word', 'worker', 'workspace', 'wrapper', 'zone',
// Common adjectives
'active', 'advanced', 'available', 'basic', 'clean', 'clear', 'complete',
'connected', 'correct', 'critical', 'current', 'dark', 'deep',
'different', 'direct', 'double', 'dynamic', 'easy', 'empty', 'entire',
'exact', 'extra', 'fast', 'final', 'fixed', 'flat', 'free', 'fresh',
'full', 'generic', 'given', 'hidden', 'initial', 'inner', 'internal',
'invalid', 'latest', 'live', 'major', 'maximum', 'minimum', 'minor',
'mixed', 'modern', 'multiple', 'native', 'natural', 'nested', 'normal',
'online', 'optional', 'outer', 'overall', 'partial', 'pending', 'plain',
'popular', 'possible', 'previous', 'private', 'proper', 'protected',
'random', 'raw', 'ready', 'real', 'recent', 'related', 'relative',
'required', 'responsive', 'safe', 'secure', 'selected', 'sensitive',
'separate', 'shared', 'silent', 'similar', 'smart', 'smooth', 'solid',
'special', 'specific', 'stable', 'static', 'strict', 'strong',
'supported', 'unique', 'universal', 'unknown', 'upper', 'valid',
'various', 'visible', 'visual', 'whole', 'wide',
// Programming / tech terms
'string', 'number', 'boolean', 'object', 'array', 'function', 'class',
'type', 'error', 'null', 'undefined', 'true', 'false', 'return',
+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 PII values.
* configured?) but CANNOT see individual PPI values.
*/
import api from './browser-polyfill.js';
@@ -198,11 +198,11 @@ const OrgPolicy = {
},
/**
* Get org-required auto-redact patterns.
* Get org-required secret scanner patterns.
*
* @returns {Array} additional patterns to add to auto-redact
* @returns {Array} additional patterns to add to the secret scanner
*/
async getOrgRedactPatterns() {
async getOrgSecretPatterns() {
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 PII values.
* Returns compliance status WITHOUT revealing actual PPI values.
*
* @returns {{ compliant: boolean, missing: string[], configured: string[] }}
*/
@@ -1,13 +1,11 @@
/**
* Silent Send - Auto Redact
* Silent Send - Secret Scanner
*
* 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
@@ -15,7 +13,7 @@
* - severity: 'critical' (always redact) or 'warning' (flag but allow)
*/
const REDACT_PATTERNS = [
const SECRET_PATTERNS = [
// --- API Keys ---
{
name: 'OpenAI API Key',
@@ -163,39 +161,14 @@ const REDACT_PATTERNS = [
},
];
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;
},
const SecretScanner = {
/**
* Scan text for secrets. Returns list of findings.
* @param {string} text
* @param {Array} [customPatterns] from settings.customRedactPatterns
*/
scan(text, customPatterns) {
scan(text) {
const findings = [];
const patterns = this._buildPatterns(customPatterns);
for (const pattern of patterns) {
for (const pattern of SECRET_PATTERNS) {
// Reset regex lastIndex
pattern.regex.lastIndex = 0;
let match;
@@ -231,11 +204,9 @@ const AutoRedact = {
/**
* 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, customPatterns) {
const findings = this.scan(text, customPatterns);
redact(text) {
const findings = this.scan(text);
const redactions = [];
let result = text;
@@ -249,7 +220,7 @@ const AutoRedact = {
redactions.push({
original: f.value,
replaced: f.redactTo,
category: 'redact',
category: 'secret',
pattern: f.name,
});
}
@@ -266,7 +237,7 @@ const AutoRedact = {
};
if (typeof globalThis !== 'undefined') {
globalThis.AutoRedact = AutoRedact;
globalThis.SecretScanner = SecretScanner;
}
export default AutoRedact;
export default SecretScanner;
+3 -4
View File
@@ -23,7 +23,7 @@ const KEYS = {
SETTINGS: 'ss_settings',
};
// Keys that contain sensitive PII and should be encrypted at rest
// Keys that contain sensitive PPI 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,13 +34,12 @@ const DEFAULT_SETTINGS = {
enabled: true,
showHighlights: false,
revealMode: false,
autoRedact: true,
secretScanning: true,
autoDetect: true,
autoRedactDetected: true,
autoAddDetected: true,
maxLogEntries: 100,
maxLogEntries: 200,
customDomains: [],
customRedactPatterns: [],
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
browserSync: false,
};
+11 -24
View File
@@ -12,9 +12,8 @@
* 5. Custom HTTP endpoint — any URL supporting GET + PUT (WebDAV,
* self-hosted server, cloud function, etc.).
*
* Encryption: all sync channels REQUIRE encryption with a password
* (AES-256-GCM) and/or TOTP verification. Syncing without encryption
* is not permitted — users must set up encryption before enabling sync.
* Encryption: all sync channels can optionally encrypt data with a
* password (AES-256-GCM) and/or require TOTP verification.
* Authentication is cached with a configurable TTL so the user only
* needs to authenticate when the cache expires and new data exists.
*
@@ -371,9 +370,6 @@ const SilentSendSync = {
const StorageModule = (await import('./storage.js')).default;
await StorageModule.decryptAllData();
// Disable all sync channels since encryption is mandatory for sync
await StorageModule.saveSettings({ browserSync: false });
await api.storage.local.remove('ss_sync_encryption');
await SilentSendCrypto.clearCachedKey();
await SilentSendCrypto.clearWebAuthnCredential();
@@ -419,7 +415,7 @@ const SilentSendSync = {
*/
async _encryptForSync(data) {
const config = await this._getSyncEncryption();
if (!config?.enabled) return { data: null, encrypted: false, needsEncryption: true };
if (!config?.enabled) return { data, encrypted: false };
const keyInfo = await this._getEncryptionKey();
if (!keyInfo) {
@@ -583,15 +579,12 @@ const SilentSendSync = {
async exportSyncCode() {
const data = await this._getAllData();
// Encrypt (mandatory)
// Encrypt if enabled
const result = await this._encryptForSync(data);
if (result.needsEncryption) {
return { needsEncryption: true };
}
if (result.needsAuth) {
return { needsAuth: true };
}
const payload = result.data;
const payload = result.data || data;
const json = JSON.stringify(payload);
return btoa(unescape(encodeURIComponent(json)));
@@ -647,10 +640,10 @@ const SilentSendSync = {
try {
const data = await this._getAllData();
// Encrypt (mandatory)
// Encrypt if enabled
const result = await this._encryptForSync(data);
if (result.needsEncryption || result.needsAuth) return; // skip — encryption required
const payload = result.data;
if (result.needsAuth) return; // silently skip — will sync on next auth
const payload = result.data || data;
const json = JSON.stringify(payload);
@@ -716,15 +709,12 @@ const SilentSendSync = {
try {
const data = await this._getAllData();
// Encrypt (mandatory)
// Encrypt if enabled
const encResult = await this._encryptForSync(data);
if (encResult.needsEncryption) {
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
}
if (encResult.needsAuth) {
return { success: false, needsAuth: true, reason: 'Authentication required.' };
}
const payload = encResult.data;
const payload = encResult.data || data;
const content = JSON.stringify(payload, null, 2);
const stored = await api.storage.local.get('ss_gist_id');
@@ -820,13 +810,10 @@ const SilentSendSync = {
const data = await this._getAllData();
const encResult = await this._encryptForSync(data);
if (encResult.needsEncryption) {
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
}
if (encResult.needsAuth) {
return { success: false, needsAuth: true, reason: 'Authentication required.' };
}
const payload = encResult.data;
const payload = encResult.data || data;
const resp = await fetch(url, {
method,
+16 -63
View File
@@ -49,38 +49,17 @@
</div>
<div class="setting-row">
<div>
<label>Auto Redact</label>
<p class="setting-desc">Automatically detect and redact API keys, tokens, passwords, SSNs, credit card numbers, and custom patterns</p>
<label>Secret scanning</label>
<p class="setting-desc">Auto-detect and redact API keys, tokens, passwords, SSNs, credit card numbers</p>
</div>
<label class="toggle">
<input type="checkbox" id="autoRedactToggle" checked>
<input type="checkbox" id="secretScanning" 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">
@@ -90,8 +69,8 @@
</div>
<div class="setting-row">
<div>
<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>
<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>
</div>
<label class="toggle">
<input type="checkbox" id="autoRedactDetected" checked>
@@ -100,8 +79,8 @@
</div>
<div class="setting-row">
<div>
<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>
<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>
@@ -113,7 +92,7 @@
<label>Max log entries</label>
<p class="setting-desc">Number of activity log entries to keep</p>
</div>
<input type="number" id="maxLogEntries" class="input-small" min="10" max="1000" value="100">
<input type="number" id="maxLogEntries" class="input-small" min="10" max="1000" value="200">
</div>
</section>
@@ -127,7 +106,7 @@
<span>&#128274;</span> Sync Encryption
</h3>
<p class="section-desc" style="margin-bottom:10px">
Encryption is required for all sync channels. Set up a password (and optionally TOTP) before enabling any sync method. Authentication is only required when new data arrives and your cached key has expired.
Encrypt your sync data with a password, TOTP, or both. Authentication is only required when new data arrives and your cached key has expired.
</p>
<div id="syncEncryptionSetup">
@@ -382,7 +361,7 @@
<!-- Organization -->
<section class="section">
<h2>Organization</h2>
<p class="section-desc">Join an organization to receive required substitution rules and auto-redact patterns from your admin. Org rules merge with your personal rules and cannot be disabled.</p>
<p class="section-desc">Join an organization to receive required substitution rules and secret scanner patterns from your admin. Org rules merge with your personal rules and cannot be disabled.</p>
<div id="orgNotJoined">
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
@@ -542,31 +521,12 @@
<h2>Custom Domains</h2>
<p class="section-desc">Add domains for self-hosted AI services (like OpenWebUI). The extension will activate on these domains in addition to the built-in ones.</p>
<div class="domain-list" id="domainList"></div>
<div class="add-row" style="flex-wrap:wrap;gap:8px">
<input type="text" id="newDomain" placeholder="https://ai.myserver.com" class="input" style="flex:2;min-width:200px">
<div class="add-row">
<input type="text" id="newDomain" placeholder="https://ai.myserver.com" class="input" style="flex:2">
<button class="btn btn-primary" id="btnAddDomain">Add Domain</button>
<button class="btn" id="btnBulkAddDomains">Bulk Add</button>
</div>
<!-- Suggested domains -->
<div style="margin-top:10px">
<label style="font-size:12px;font-weight:500;color:#374151;display:block;margin-bottom:4px">Quick add popular sites:</label>
<div id="suggestedDomains" style="display:flex;flex-wrap:wrap;gap:4px"></div>
</div>
<!-- Bulk add textarea (hidden by default) -->
<div id="bulkDomainSection" style="display:none;margin-top:10px;padding:10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px">
<label style="font-size:12px;font-weight:500;color:#374151;display:block;margin-bottom:4px">Paste domains (one per line):</label>
<textarea id="bulkDomainText" rows="5" placeholder="https://ai.example.com&#10;https://openwebui.local&#10;myai.company.com" style="width:100%;box-sizing:border-box;font-size:12px;font-family:monospace;padding:8px;border:1px solid #d1d5db;border-radius:6px;resize:vertical"></textarea>
<div style="display:flex;gap:8px;margin-top:6px">
<button class="btn btn-primary btn-sm" id="btnApplyBulkDomains">Add All</button>
<button class="btn btn-sm" id="btnCancelBulkDomains">Cancel</button>
<span id="bulkDomainStatus" style="font-size:11px;color:#6b7280;align-self:center"></span>
</div>
</div>
<p class="section-desc" style="margin-top:8px;margin-bottom:0">
When you add a domain, your browser will ask you to confirm access. No extra steps needed.
When you click "Add Domain", your browser will ask you to confirm access. No extra steps needed.
</p>
</section>
@@ -631,14 +591,7 @@
</section>
<footer>
<p>Silent Send v0.9.20</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>
<p>Silent Send v0.3.0</p>
</footer>
</div>
+57 -340
View File
@@ -14,23 +14,17 @@ let passwordsRevealed = false;
const $ = (sel) => document.querySelector(sel);
// --- Safe innerHTML replacement (AMO-compliant) ---
function safeHTML(el, html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
el.replaceChildren(...Array.from(doc.body.childNodes));
}
document.addEventListener('DOMContentLoaded', async () => {
mappings = await Storage.getMappings();
settings = await Storage.getSettings();
// Apply settings to UI
$('#showHighlights').checked = settings.showHighlights || false;
$('#autoRedactToggle').checked = settings.autoRedact !== false;
$('#secretScanning').checked = settings.secretScanning !== false;
$('#autoDetect').checked = settings.autoDetect !== false;
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
$('#maxLogEntries').value = settings.maxLogEntries || 100;
$('#maxLogEntries').value = settings.maxLogEntries || 200;
$('#browserSync').checked = settings.browserSync === true;
renderMappings();
@@ -51,28 +45,17 @@ document.addEventListener('DOMContentLoaded', async () => {
// --- Sync section ---
$('#browserSync').addEventListener('change', async (e) => {
await Storage.saveSettings({ browserSync: e.target.checked });
if (e.target.checked) {
const encEnabled = await SilentSendSync.isEncryptionEnabled();
if (!encEnabled) {
e.target.checked = false;
setSyncStatus('Encryption must be enabled before syncing. Set up encryption first.', 'error');
return;
}
await Storage.saveSettings({ browserSync: true });
await SilentSendSync.pushToSyncStorage();
setSyncStatus('Browser account sync enabled. Your settings will sync automatically.', 'ok');
} else {
await Storage.saveSettings({ browserSync: false });
setSyncStatus('Browser account sync disabled.', 'neutral');
}
});
$('#btnGenerateSyncCode').addEventListener('click', async () => {
const code = await SilentSendSync.exportSyncCode();
if (code?.needsEncryption) {
setSyncStatus('Encryption must be enabled before syncing. Set up encryption first.', 'error');
return;
}
if (code?.needsAuth) {
setSyncStatus('Authentication required to encrypt sync code.', 'warn');
showSyncAuthPrompt();
@@ -185,9 +168,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (!token) { setGistSyncStatus('Enter your GitHub PAT first.', 'warn'); return; }
setGistSyncStatus('Pushing…', 'neutral');
const r = await SilentSendSync.pushToGist(token);
if (r.needsEncryption) {
setGistSyncStatus('Encryption must be enabled before syncing.', 'error');
} else if (r.needsAuth) {
if (r.needsAuth) {
setGistSyncStatus('Authentication required to encrypt.', 'warn');
showSyncAuthPrompt();
} else if (r.success) {
@@ -226,9 +207,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const headers = parseHeadersField($('#customSyncHeaders').value);
setUrlSyncStatus('Pushing…', 'neutral');
const r = await SilentSendSync.pushToUrl({ url, headers });
if (r.needsEncryption) {
setUrlSyncStatus('Encryption must be enabled before syncing.', 'error');
} else if (r.needsAuth) {
if (r.needsAuth) {
setUrlSyncStatus('Authentication required to encrypt.', 'warn');
showSyncAuthPrompt();
} else if (r.success) {
@@ -280,34 +259,14 @@ document.addEventListener('DOMContentLoaded', async () => {
$('#newDomain').addEventListener('keydown', (e) => {
if (e.key === 'Enter') addDomain();
});
renderSuggestedDomains();
// Bulk add domains
$('#btnBulkAddDomains').addEventListener('click', () => {
const section = $('#bulkDomainSection');
section.style.display = section.style.display === 'none' ? 'block' : 'none';
});
$('#btnCancelBulkDomains').addEventListener('click', () => {
$('#bulkDomainSection').style.display = 'none';
$('#bulkDomainText').value = '';
$('#bulkDomainStatus').textContent = '';
});
$('#btnApplyBulkDomains').addEventListener('click', bulkAddDomains);
// Settings listeners
$('#showHighlights').addEventListener('change', async (e) => {
await Storage.saveSettings({ showHighlights: 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();
$('#secretScanning').addEventListener('change', async (e) => {
await Storage.saveSettings({ secretScanning: e.target.checked });
});
$('#autoDetect').addEventListener('change', async (e) => {
@@ -323,7 +282,7 @@ document.addEventListener('DOMContentLoaded', async () => {
});
$('#maxLogEntries').addEventListener('change', async (e) => {
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 100 });
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 });
});
// Add mapping
@@ -452,11 +411,11 @@ function renderMappings() {
const nonPasswordMappings = mappings.filter(m => m.category !== 'password');
if (nonPasswordMappings.length === 0) {
safeHTML(tbody, '<tr><td colspan="6" style="text-align:center;color:#9ca3af;padding:24px">No mappings configured</td></tr>');
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#9ca3af;padding:24px">No mappings configured</td></tr>';
return;
}
safeHTML(tbody, nonPasswordMappings
tbody.innerHTML = nonPasswordMappings
.map(
(m) => `
<tr data-id="${m.id}">
@@ -474,7 +433,7 @@ function renderMappings() {
</tr>
`
)
.join(''));
.join('');
// Bind
tbody.querySelectorAll('.btn-delete').forEach((btn) => {
@@ -504,14 +463,14 @@ function renderPasswords() {
const noMsg = $('#noPasswordsMsg');
if (passwordMappings.length === 0) {
tbody.replaceChildren();
tbody.innerHTML = '';
noMsg.style.display = 'block';
return;
}
noMsg.style.display = 'none';
safeHTML(tbody, passwordMappings.map(m => {
tbody.innerHTML = passwordMappings.map(m => {
const displayReal = passwordsRevealed
? escapeHtml(m.real)
: '&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;';
@@ -528,7 +487,7 @@ function renderPasswords() {
</td>
<td><button class="btn btn-sm btn-danger btn-delete-pw">&times;</button></td>
</tr>`;
}).join(''));
}).join('');
// Bind delete
tbody.querySelectorAll('.btn-delete-pw').forEach(btn => {
@@ -565,11 +524,11 @@ async function renderLog() {
const list = $('#logList');
if (log.length === 0) {
safeHTML(list, '<div style="text-align:center;color:#9ca3af;padding:24px">No activity logged</div>');
list.innerHTML = '<div style="text-align:center;color:#9ca3af;padding:24px">No activity logged</div>';
return;
}
safeHTML(list, log
list.innerHTML = log
.slice(0, 100)
.map((entry) => {
const time = new Date(entry.timestamp).toLocaleString();
@@ -582,315 +541,74 @@ async function renderLog() {
</div>
`;
})
.join(''));
.join('');
}
// --- Custom Domains ---
async function addDomain() {
let domain = $('#newDomain').value.trim();
if (!domain) return;
// Suggested popular domains (not already built-in)
const SUGGESTED_DOMAINS = [
{ label: 'OpenWebUI', url: 'https://openwebui.local' },
{ label: 'Ollama Web', url: 'http://localhost:3000' },
{ label: 'text-generation-webui', url: 'http://localhost:7860' },
{ label: 'Jan.ai', url: 'https://jan.ai' },
{ label: 'You.com', url: 'https://you.com' },
{ label: 'Phind', url: 'https://www.phind.com' },
{ label: 'Cohere', url: 'https://coral.cohere.com' },
{ label: 'Mistral', url: 'https://chat.mistral.ai' },
{ label: 'Pi AI', url: 'https://pi.ai' },
{ label: 'Notion AI', url: 'https://www.notion.so' },
{ label: 'Quora', url: 'https://www.quora.com' },
{ label: 'Discord', url: 'https://discord.com' },
{ label: 'Slack', url: 'https://app.slack.com' },
{ label: 'Jira', url: 'https://atlassian.net' },
{ label: 'Linear', url: 'https://linear.app' },
{ label: 'Bitbucket', url: 'https://bitbucket.org' },
];
function normalizeDomain(raw) {
let domain = raw.trim();
if (!domain) return null;
// Normalize: ensure it has a protocol
if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
domain = 'https://' + domain;
}
return domain.replace(/\/+$/, '');
}
// Strip trailing slashes
domain = domain.replace(/\/+$/, '');
async function addSingleDomain(domain) {
const domains = settings.customDomains || [];
if (domains.includes(domain)) return { added: false, reason: 'duplicate' };
if (domains.includes(domain)) {
alert('Domain already added.');
return;
}
// Request browser permission for this domain
try {
const granted = await api.permissions.request({ origins: [domain + '/*'] });
if (!granted) return { added: false, reason: 'denied' };
const granted = await api.permissions.request({
origins: [domain + '/*'],
});
if (!granted) {
alert('Permission denied. The extension needs access to this domain to work.');
return;
}
} catch (e) {
// Firefox or older Chrome may not support optional permissions this way
console.warn('[Silent Send] Could not request permission:', e);
}
domains.push(domain);
settings.customDomains = domains;
await Storage.saveSettings({ customDomains: domains });
return { added: true };
}
async function addDomain() {
const domain = normalizeDomain($('#newDomain').value);
if (!domain) return;
const result = await addSingleDomain(domain);
if (!result.added) {
if (result.reason === 'duplicate') alert('Domain already added.');
else alert('Permission denied. The extension needs access to this domain to work.');
return;
}
renderDomains();
renderSuggestedDomains();
$('#newDomain').value = '';
}
async function bulkAddDomains() {
const text = $('#bulkDomainText').value;
const lines = text.split(/[\n,]+/).map(l => l.trim()).filter(Boolean);
if (lines.length === 0) return;
let added = 0, skipped = 0;
for (const line of lines) {
const domain = normalizeDomain(line);
if (!domain) { skipped++; continue; }
const result = await addSingleDomain(domain);
if (result.added) added++;
else skipped++;
}
renderDomains();
renderSuggestedDomains();
$('#bulkDomainStatus').textContent = `Added ${added}, skipped ${skipped}`;
if (added > 0) $('#bulkDomainText').value = '';
}
function renderSuggestedDomains() {
const container = $('#suggestedDomains');
if (!container) return;
const domains = settings.customDomains || [];
// Filter out suggestions that are already added
const available = SUGGESTED_DOMAINS.filter(s => !domains.includes(s.url));
if (available.length === 0) {
safeHTML(container, '<span style="font-size:11px;color:#9ca3af">All suggestions added!</span>');
return;
}
safeHTML(container, available.map(s =>
`<button class="btn-suggest-domain" data-url="${escapeHtml(s.url)}" title="${escapeHtml(s.url)}" style="font-size:11px;padding:3px 8px;border:1px solid #d1d5db;border-radius:12px;background:#fff;cursor:pointer;color:#374151;white-space:nowrap">+ ${escapeHtml(s.label)}</button>`
).join(''));
container.querySelectorAll('.btn-suggest-domain').forEach(btn => {
btn.addEventListener('click', async () => {
const domain = btn.dataset.url;
const result = await addSingleDomain(domain);
if (result.added) {
renderDomains();
renderSuggestedDomains();
} else if (result.reason === 'denied') {
alert('Permission denied for ' + domain);
}
});
});
}
function renderDomains() {
const list = $('#domainList');
const domains = settings.customDomains || [];
if (domains.length === 0) {
safeHTML(list, '<div style="text-align:center;color:#9ca3af;padding:12px;font-size:13px">No custom domains. Built-in sites (Claude, ChatGPT, Gemini, Grok, Perplexity, Copilot, DeepSeek, HuggingChat, Poe, GitHub, GitLab, Reddit, Stack Overflow, Pastebin, localhost) are always active.</div>');
list.innerHTML = '<div style="text-align:center;color:#9ca3af;padding:12px;font-size:13px">No custom domains. Built-in sites (Claude, ChatGPT, Grok, Gemini, localhost) are always active.</div>';
return;
}
safeHTML(list, domains
list.innerHTML = domains
.map((d, i) => `
<div class="domain-item" style="display:flex;align-items:center;justify-content:space-between;padding:8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
<span class="domain-text" style="font-size:13px;font-family:monospace;flex:1;overflow:hidden;text-overflow:ellipsis">${escapeHtml(d)}</span>
<div style="display:flex;gap:4px;margin-left:8px">
<button class="btn btn-sm btn-edit-domain" data-index="${i}" title="Edit">&#9998;</button>
<button class="btn btn-sm btn-danger btn-remove-domain" data-index="${i}" title="Remove">&times;</button>
</div>
<span style="font-size:13px;font-family:monospace">${escapeHtml(d)}</span>
<button class="btn btn-sm btn-danger btn-remove-domain" data-index="${i}">&times;</button>
</div>
`)
.join(''));
.join('');
// Edit handlers
list.querySelectorAll('.btn-edit-domain').forEach((btn) => {
btn.addEventListener('click', async () => {
const idx = parseInt(btn.dataset.index, 10);
const domains = settings.customDomains || [];
const current = domains[idx];
const row = btn.closest('.domain-item');
const textEl = row.querySelector('.domain-text');
// Replace text with input
const input = document.createElement('input');
input.type = 'text';
input.value = current;
input.style.cssText = 'flex:1;font-size:12px;font-family:monospace;padding:3px 6px;border:1px solid #3b82f6;border-radius:4px;outline:none;min-width:0';
textEl.replaceWith(input);
input.focus();
input.select();
// Replace edit button with save button
btn.textContent = '\u2713';
btn.title = 'Save';
btn.style.color = '#10b981';
const save = async () => {
const newDomain = normalizeDomain(input.value);
if (!newDomain || newDomain === current) {
renderDomains();
return;
}
if (domains.includes(newDomain)) {
alert('Domain already exists.');
renderDomains();
return;
}
// Request permission for new domain
try {
const granted = await api.permissions.request({ origins: [newDomain + '/*'] });
if (!granted) {
alert('Permission denied for ' + newDomain);
renderDomains();
return;
}
} catch (e) { /* non-fatal */ }
// Revoke old permission
try {
await api.permissions.remove({ origins: [current + '/*'] });
} catch (e) { /* non-fatal */ }
domains[idx] = newDomain;
settings.customDomains = domains;
await Storage.saveSettings({ customDomains: domains });
renderDomains();
renderSuggestedDomains();
};
btn.onclick = save;
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') save();
if (e.key === 'Escape') renderDomains();
});
input.addEventListener('blur', () => {
// Small delay to allow button click to fire first
setTimeout(() => { if (document.contains(input)) renderDomains(); }, 150);
});
});
});
// Remove handlers
list.querySelectorAll('.btn-remove-domain').forEach((btn) => {
btn.addEventListener('click', async () => {
const idx = parseInt(btn.dataset.index, 10);
const domains = settings.customDomains || [];
const removed = domains.splice(idx, 1)[0];
domains.splice(idx, 1);
settings.customDomains = domains;
await Storage.saveSettings({ customDomains: domains });
if (removed) {
try {
await api.permissions.remove({ origins: [removed + '/*'] });
} catch (e) { /* non-fatal */ }
}
renderDomains();
renderSuggestedDomains();
});
});
}
// --- 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();
});
});
}
@@ -1241,9 +959,8 @@ async function initSyncEncryptionUI() {
$('#btnDisableEncryption').addEventListener('click', async () => {
if (!window.confirm('Disable sync encryption? Existing encrypted sync data will become unreadable.')) return;
await SilentSendSync.disableEncryption();
$('#browserSync').checked = false;
showEncryptionNotConfigured();
setSyncEncStatus('Encryption disabled. All sync channels have been turned off.', 'neutral');
setSyncEncStatus('Encryption disabled.', 'neutral');
});
// Change password
@@ -1578,11 +1295,11 @@ async function renderVersionHistory() {
const snapshots = await VersionHistory.getSnapshots();
if (snapshots.length === 0) {
safeHTML(list, '<div style="text-align:center;color:#9ca3af;padding:12px">No snapshots yet. Snapshots are created on each sync.</div>');
list.innerHTML = '<div style="text-align:center;color:#9ca3af;padding:12px">No snapshots yet. Snapshots are created on each sync.</div>';
return;
}
safeHTML(list, snapshots.map(s => {
list.innerHTML = snapshots.map(s => {
const time = new Date(s.timestamp).toLocaleString();
const mappingCount = (s.data?.mappings || []).length;
return `<div style="display:flex;align-items:center;justify-content:space-between;padding:8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
@@ -1593,7 +1310,7 @@ async function renderVersionHistory() {
</div>
<button class="btn btn-sm btn-restore-snapshot" data-id="${s.id}">Restore</button>
</div>`;
}).join(''));
}).join('');
list.querySelectorAll('.btn-restore-snapshot').forEach(btn => {
btn.addEventListener('click', async () => {
@@ -1638,13 +1355,13 @@ async function renderDevices() {
const entries = Object.values(devices);
if (entries.length === 0) {
safeHTML(list, '<div style="text-align:center;color:#9ca3af;padding:12px">No devices synced yet. Push or pull to register this device.</div>');
list.innerHTML = '<div style="text-align:center;color:#9ca3af;padding:12px">No devices synced yet. Push or pull to register this device.</div>';
return;
}
entries.sort((a, b) => (b.lastSync || 0) - (a.lastSync || 0));
safeHTML(list, `<table style="width:100%;font-size:12px;border-collapse:collapse">
list.innerHTML = `<table style="width:100%;font-size:12px;border-collapse:collapse">
<thead><tr style="text-align:left;border-bottom:1px solid #e5e7eb">
<th style="padding:6px">Device</th>
<th style="padding:6px">Browser</th>
@@ -1661,7 +1378,7 @@ async function renderDevices() {
<td style="padding:6px">${!isCurrent ? `<button class="btn btn-sm btn-danger btn-remove-device" data-id="${d.id}">&times;</button>` : ''}</td>
</tr>`;
}).join('')}</tbody>
</table>`);
</table>`;
list.querySelectorAll('.btn-remove-device').forEach(btn => {
btn.addEventListener('click', async () => {
@@ -1738,9 +1455,9 @@ async function showOrgJoined() {
const compliance = await OrgPolicy.checkCompliance();
const statusEl = $('#orgComplianceStatus');
if (compliance.compliant) {
safeHTML(statusEl, '<span style="color:#10b981">&#10003; Compliant — all required fields configured</span>');
statusEl.innerHTML = '<span style="color:#10b981">&#10003; Compliant — all required fields configured</span>';
} else {
safeHTML(statusEl, `<span style="color:#b45309">Missing: ${compliance.missing.join(', ')}</span>`);
statusEl.innerHTML = `<span style="color:#b45309">Missing: ${compliance.missing.join(', ')}</span>`;
}
const reqMappings = policy?.requiredMappings || [];
@@ -1892,7 +1609,7 @@ async function checkConflicts() {
function renderConflicts(conflicts) {
const list = $('#conflictList');
safeHTML(list, conflicts.map(c => `
list.innerHTML = conflicts.map(c => `
<div style="padding:10px;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;margin-bottom:8px" data-conflict-id="${c.id}">
<div style="font-size:12px;font-weight:500;margin-bottom:6px">${escapeHtml(c.path)}</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:8px">
@@ -1910,7 +1627,7 @@ function renderConflicts(conflicts) {
<button class="btn btn-sm btn-resolve" data-id="${c.id}" data-choice="remote">Keep Remote</button>
</div>
</div>
`).join(''));
`).join('');
list.querySelectorAll('.btn-resolve').forEach(btn => {
btn.addEventListener('click', async () => {
@@ -1971,10 +1688,10 @@ async function handleBulkImport(e) {
result.identity.usernames.filter(u => !u.substitute).length +
result.identity.phones.filter(p => !p.substitute).length;
safeHTML($('#bulkImportSummary'), `
$('#bulkImportSummary').innerHTML = `
Found: ${parts.join(', ')}.
${needsMapping > 0 ? `<span style="color:#b45309">${needsMapping} item(s) need substitutes — you can add them after import.</span>` : ''}
`);
`;
// Build preview list
const items = [];
@@ -1994,8 +1711,8 @@ async function handleBulkImport(e) {
items.push(`<div><span style="color:#6b7280">${escapeHtml(m.category)}:</span> <strong>${escapeHtml(m.real)}</strong>${m.substitute ? ' → ' + escapeHtml(m.substitute) : ' <span style="color:#b45309">needs substitute</span>'}</div>`);
}
safeHTML($('#bulkImportItems'), items.slice(0, 50).join('') +
(items.length > 50 ? `<div style="color:#6b7280;margin-top:4px">+${items.length - 50} more...</div>` : ''));
$('#bulkImportItems').innerHTML = items.slice(0, 50).join('') +
(items.length > 50 ? `<div style="color:#6b7280;margin-top:4px">+${items.length - 50} more...</div>` : '');
$('#bulkImportPreview').style.display = 'block';
-29
View File
@@ -623,35 +623,6 @@ body {
font-size: 12px;
}
/* Options tab */
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f3f4f6;
}
.setting-item:last-of-type { border-bottom: none; }
.setting-label {
flex: 1;
min-width: 0;
}
.setting-label strong {
font-size: 12px;
display: block;
color: #1f2937;
}
.setting-label .setting-desc {
font-size: 10px;
color: #9ca3af;
display: block;
margin-top: 1px;
}
/* Footer */
.footer {
padding: 8px 16px;
+4 -77
View File
@@ -33,7 +33,6 @@
<button class="tab" data-tab="mappings">Mappings</button>
<button class="tab" data-tab="activity">Activity</button>
<button class="tab" data-tab="test">Test</button>
<button class="tab" data-tab="options">Options</button>
</nav>
<!-- Locked State Overlay -->
@@ -194,75 +193,6 @@
<div class="test-identity-status" id="identityStatus"></div>
</section>
<!-- Options Tab -->
<section class="tab-content" id="tab-options">
<div class="setting-item">
<div class="setting-label">
<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="optAutoRedact" checked><span class="toggle-slider"></span></label>
</div>
<div class="setting-item">
<div class="setting-label">
<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>
</div>
<div class="setting-item">
<div class="setting-label">
<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="optAutoRedactDetected" checked><span class="toggle-slider"></span></label>
</div>
<div class="setting-item">
<div class="setting-label">
<strong>Show highlights</strong>
<span class="setting-desc">Highlight substituted values in AI responses</span>
</div>
<label class="toggle"><input type="checkbox" id="optHighlights"><span class="toggle-slider"></span></label>
</div>
<div class="setting-item">
<div class="setting-label">
<strong>Document scan preview</strong>
<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>
<div class="setting-item">
<div class="setting-label">
<strong>Detect proper nouns</strong>
<span class="setting-desc">Flag capitalized phrases (names, companies) — may produce false positives</span>
</div>
<label class="toggle"><input type="checkbox" id="optProperNouns"><span class="toggle-slider"></span></label>
</div>
<div style="margin-top:12px;padding-top:10px;border-top:1px solid #e5e7eb">
<div class="setting-label" style="margin-bottom:6px">
<strong>Custom Domains</strong>
<span class="setting-desc">Add sites beyond the built-in list</span>
</div>
<div id="popupDomainList" style="max-height:120px;overflow-y:auto;margin-bottom:6px"></div>
<div style="display:flex;gap:4px">
<input type="text" id="popupNewDomain" placeholder="https://ai.example.com" style="flex:1;font-size:11px;padding:4px 6px;border:1px solid #d1d5db;border-radius:4px;min-width:0">
<button class="btn" id="btnPopupAddDomain" style="font-size:11px;padding:4px 8px;white-space:nowrap">Add</button>
</div>
<div id="popupDomainSuggestions" style="margin-top:6px;display:flex;flex-wrap:wrap;gap:3px"></div>
</div>
<div style="margin-top:12px;padding-top:10px;border-top:1px solid #e5e7eb">
<button class="btn" id="btnOpenFullOptions" style="width:100%;font-size:12px">Open Full Options Page</button>
<p class="help-text" style="margin-top:6px;text-align:center">Sync, encryption, org, version history, import/export, and more</p>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="privacy-note">
@@ -271,14 +201,11 @@
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 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.
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.
</div>
<a href="#" id="btnOptions">Options</a>
</footer>
</div>
+41 -196
View File
@@ -1,6 +1,6 @@
import SubstitutionEngine from '../lib/substitution-engine.js';
import SmartPatterns from '../lib/smart-patterns.js';
import AutoRedact from '../lib/auto-redact.js';
import SecretScanner from '../lib/secret-scanner.js';
import AutoDetect from '../lib/auto-detect.js';
import Storage from '../lib/storage.js';
import SilentSendSync from '../lib/sync.js';
@@ -18,12 +18,6 @@ let settings = {};
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
// --- Safe innerHTML replacement (AMO-compliant) ---
function safeHTML(el, html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
el.replaceChildren(...Array.from(doc.body.childNodes));
}
// --- Init ---
document.addEventListener('DOMContentLoaded', async () => {
// Check if locked BEFORE trying to read sensitive data
@@ -208,45 +202,12 @@ async function initUnlockedUI() {
});
});
// Open full options page button (Options tab)
$('#btnOpenFullOptions').addEventListener('click', () => {
// Options link
$('#btnOptions').addEventListener('click', (e) => {
e.preventDefault();
api.runtime.openOptionsPage();
});
// Load options tab settings
$('#optAutoRedact').checked = settings.autoRedact !== false;
$('#optAutoDetect').checked = settings.autoDetect !== false;
$('#optAutoRedactDetected').checked = settings.autoRedactDetected !== false;
$('#optHighlights').checked = settings.showHighlights || false;
$('#optDocPreview').checked = settings.docScanPreview !== false;
$('#optProperNouns').checked = settings.detectProperNouns || false;
// Options tab change handlers
const optHandlers = [
['optAutoRedact', 'autoRedact'],
['optAutoDetect', 'autoDetect'],
['optAutoRedactDetected', 'autoRedactDetected'],
['optHighlights', 'showHighlights'],
['optDocPreview', 'docScanPreview'],
['optProperNouns', 'detectProperNouns'],
];
for (const [id, key] of optHandlers) {
$(`#${id}`).addEventListener('change', async (e) => {
settings[key] = e.target.checked;
await Storage.saveSettings({ [key]: e.target.checked });
api.runtime.sendMessage({ type: 'update:settings', settings });
});
}
// --- Popup domain management ---
renderPopupDomains();
renderPopupDomainSuggestions();
$('#btnPopupAddDomain').addEventListener('click', popupAddDomain);
$('#popupNewDomain').addEventListener('keydown', (e) => {
if (e.key === 'Enter') popupAddDomain();
});
// Update privacy note based on encryption state
const encEnabled = await Storage._isAtRestEncryptionEnabled();
const encNote = $('#privacyEncNote');
@@ -341,17 +302,11 @@ async function showLockedUI() {
// --- Profiles ---
function renderProfileSelector() {
const select = $('#profileSelect');
// Build options via DOM API — safeHTML + DOMParser mangles <option> elements
select.replaceChildren();
for (const p of profiles) {
const opt = new Option(
`${p.name}${p.active ? '' : ' (off)'}`,
p.id,
false,
p.id === currentProfileId
);
select.appendChild(opt);
}
select.innerHTML = profiles.map(p =>
`<option value="${p.id}" ${p.id === currentProfileId ? 'selected' : ''}>` +
`${escapeHtml(p.name)}${p.active ? '' : ' (off)'}` +
`</option>`
).join('');
const profile = profiles.find(p => p.id === currentProfileId);
$('#profileActive').checked = profile?.active ?? true;
@@ -400,7 +355,7 @@ function renderFieldList(fieldName, items) {
items = [{ real: '', substitute: '', type: config.defaultType || '' }];
}
safeHTML(container, items.map((item, i) => {
container.innerHTML = items.map((item, i) => {
let typeHtml = '';
if (config.typeOptions) {
typeHtml = `<select class="id-type-select" data-index="${i}" style="padding:3px 2px;font-size:10px;border:1px solid #e5e7eb;border-radius:3px;width:42px">` +
@@ -416,7 +371,7 @@ function renderFieldList(fieldName, items) {
<input type="text" class="input input-sm id-sub" value="${escapeAttr(item.substitute || '')}" placeholder="${config.placeholderSub}">
<button class="btn-remove" title="Remove">&times;</button>
</div>`;
}).join(''));
}).join('');
// Bind remove buttons
container.querySelectorAll('.btn-remove').forEach(btn => {
@@ -465,13 +420,13 @@ function loadIdentityForm() {
).join('') +
`</select>`;
}
safeHTML(tempDiv, `<div class="id-entry-row" data-index="${count}">
tempDiv.innerHTML = `<div class="id-entry-row" data-index="${count}">
${typeHtml}
<input type="text" class="input input-sm id-real" placeholder="${config.placeholderReal}">
<span class="arrow" style="font-size:12px">&rarr;</span>
<input type="text" class="input input-sm id-sub" placeholder="${config.placeholderSub}">
<button class="btn-remove" title="Remove">&times;</button>
</div>`);
</div>`;
const row = tempDiv.firstElementChild;
container.appendChild(row);
row.querySelector('.btn-remove').addEventListener('click', () => {
@@ -621,11 +576,11 @@ function renderMappings() {
const list = $('#mappingList');
if (mappings.length === 0) {
safeHTML(list, '<div class="empty-state">No mappings yet. Add your first one above.</div>');
list.innerHTML = '<div class="empty-state">No mappings yet. Add your first one above.</div>';
return;
}
safeHTML(list, mappings
list.innerHTML = mappings
.map(
(m) => `
<div class="mapping-item" data-id="${m.id}">
@@ -642,7 +597,7 @@ function renderMappings() {
</div>
`
)
.join(''));
.join('');
// Bind actions
list.querySelectorAll('.btn-delete').forEach((btn) => {
@@ -676,11 +631,11 @@ async function renderActivity() {
countEl.textContent = `${log.length} substitution${log.length !== 1 ? 's' : ''} logged`;
if (log.length === 0) {
safeHTML(list, '<div class="empty-state">No activity yet.</div>');
list.innerHTML = '<div class="empty-state">No activity yet.</div>';
return;
}
safeHTML(list, log
list.innerHTML = log
.slice(0, 50)
.map((entry) => {
const time = new Date(entry.timestamp).toLocaleTimeString([], {
@@ -698,7 +653,7 @@ async function renderActivity() {
</div>
`;
})
.join(''));
.join('');
}
// --- Test Diff (Strip: real → fake) ---
@@ -708,23 +663,23 @@ function renderTestDiff() {
const stats = $('#diffStats');
if (!input) {
output.replaceChildren();
output.innerHTML = '';
stats.textContent = '';
return;
}
const smartResult = SmartPatterns.substitute(input, identity);
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
const redactResult = AutoRedact.redact(explicitResult.text, settings.customRedactPatterns);
const secretResult = SecretScanner.redact(explicitResult.text);
const allReplacements = [
...smartResult.replacements,
...explicitResult.replacements,
...redactResult.redactions,
...secretResult.redactions,
];
const finalText = redactResult.text;
const finalText = secretResult.text;
if (finalText === input && redactResult.warnings.length === 0) {
if (finalText === input && secretResult.warnings.length === 0) {
output.textContent = input;
stats.textContent = 'No substitutions detected';
return;
@@ -740,40 +695,38 @@ function renderTestDiff() {
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
);
}
// Highlight auto-redactions in red
for (const r of redactResult.redactions) {
// Highlight secret redactions in red
for (const r of secretResult.redactions) {
const escapedReplaced = escapeHtml(r.replaced);
html = html.replace(
escapedReplaced,
`<span class="sub-highlight" style="background:#fee2e2;color:#dc2626" title="${escapeHtml(r.pattern)}">${escapedReplaced}</span>`
);
}
safeHTML(output, html);
output.innerHTML = html;
const smartCount = smartResult.replacements.length;
const explicitCount = explicitResult.replacements.length;
const redactCount = redactResult.redactions.length;
const warnCount = redactResult.warnings.length;
const secretCount = secretResult.redactions.length;
const warnCount = secretResult.warnings.length;
const parts = [];
if (smartCount > 0) parts.push(`${smartCount} smart`);
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
if (redactCount > 0) parts.push(`${redactCount} auto-redacted`);
if (secretCount > 0) parts.push(`${secretCount} secrets redacted`);
if (warnCount > 0) parts.push(`${warnCount} warnings`);
// Auto-detect unconfigured PII in the final text
const piiWarnings = AutoDetect.scan(finalText, identity);
if (piiWarnings.length > 0) parts.push(`${piiWarnings.length} PII detected`);
// Auto-detect unconfigured PPI in the final text
const ppiWarnings = AutoDetect.scan(finalText, identity);
if (ppiWarnings.length > 0) parts.push(`${ppiWarnings.length} PPI detected`);
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.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(piiDiv);
// Show PPI warnings below stats
if (ppiWarnings.length > 0) {
stats.innerHTML += `<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('')}
</div>`;
}
}
@@ -784,7 +737,7 @@ function renderRevealDiff() {
const stats = $('#revealStats');
if (!input) {
output.replaceChildren();
output.innerHTML = '';
stats.textContent = '';
return;
}
@@ -830,7 +783,7 @@ function renderRevealDiff() {
`<span class="sub-highlight" title="Was: ${escapeHtml(pair.substitute)}" style="background:#dbeafe;color:#1d4ed8">${escapedReal}</span>`
);
}
safeHTML(output, html);
output.innerHTML = html;
stats.textContent = `${totalCount} value${totalCount !== 1 ? 's' : ''} revealed`;
}
@@ -911,114 +864,6 @@ function updateStatusDot() {
dot.classList.toggle('disabled', !settings.enabled);
}
// --- Popup Domain Management ---
const POPUP_SUGGESTED_DOMAINS = [
{ label: 'Mistral', url: 'https://chat.mistral.ai' },
{ label: 'Cohere', url: 'https://coral.cohere.com' },
{ label: 'Phind', url: 'https://www.phind.com' },
{ label: 'You.com', url: 'https://you.com' },
{ label: 'Pi AI', url: 'https://pi.ai' },
{ label: 'Discord', url: 'https://discord.com' },
{ label: 'Slack', url: 'https://app.slack.com' },
{ label: 'Notion', url: 'https://www.notion.so' },
{ label: 'Linear', url: 'https://linear.app' },
{ label: 'Bitbucket', url: 'https://bitbucket.org' },
];
function normalizeDomain(raw) {
let d = raw.trim();
if (!d) return null;
if (!d.startsWith('http://') && !d.startsWith('https://')) d = 'https://' + d;
return d.replace(/\/+$/, '');
}
async function popupAddDomain() {
const domain = normalizeDomain($('#popupNewDomain').value);
if (!domain) return;
const domains = settings.customDomains || [];
if (domains.includes(domain)) return;
try {
const granted = await api.permissions.request({ origins: [domain + '/*'] });
if (!granted) return;
} catch (e) { /* non-fatal */ }
domains.push(domain);
settings.customDomains = domains;
await Storage.saveSettings({ customDomains: domains });
$('#popupNewDomain').value = '';
renderPopupDomains();
renderPopupDomainSuggestions();
}
function renderPopupDomains() {
const list = $('#popupDomainList');
if (!list) return;
const domains = settings.customDomains || [];
if (domains.length === 0) {
safeHTML(list, '<div style="font-size:11px;color:#9ca3af;padding:4px 0">No custom domains added</div>');
return;
}
safeHTML(list, domains.map((d, i) => `
<div style="display:flex;align-items:center;justify-content:space-between;padding:3px 6px;background:#f9fafb;border-radius:4px;margin-bottom:2px">
<span style="font-size:11px;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1">${escapeHtml(d)}</span>
<button class="btn-popup-remove-domain" data-index="${i}" style="border:none;background:none;color:#9ca3af;cursor:pointer;font-size:14px;padding:0 2px;line-height:1" title="Remove">&times;</button>
</div>
`).join(''));
list.querySelectorAll('.btn-popup-remove-domain').forEach(btn => {
btn.addEventListener('click', async () => {
const idx = parseInt(btn.dataset.index, 10);
const domains = settings.customDomains || [];
const removed = domains.splice(idx, 1)[0];
settings.customDomains = domains;
await Storage.saveSettings({ customDomains: domains });
if (removed) {
try { await api.permissions.remove({ origins: [removed + '/*'] }); } catch (e) { /* non-fatal */ }
}
renderPopupDomains();
renderPopupDomainSuggestions();
});
});
}
function renderPopupDomainSuggestions() {
const container = $('#popupDomainSuggestions');
if (!container) return;
const domains = settings.customDomains || [];
const available = POPUP_SUGGESTED_DOMAINS.filter(s => !domains.includes(s.url));
if (available.length === 0) {
container.replaceChildren();
return;
}
safeHTML(container, available.slice(0, 6).map(s =>
`<button class="btn-popup-suggest" data-url="${escapeHtml(s.url)}" title="${escapeHtml(s.url)}" style="font-size:10px;padding:2px 6px;border:1px solid #e5e7eb;border-radius:10px;background:#fff;cursor:pointer;color:#6b7280;white-space:nowrap">+ ${escapeHtml(s.label)}</button>`
).join(''));
container.querySelectorAll('.btn-popup-suggest').forEach(btn => {
btn.addEventListener('click', async () => {
const domain = btn.dataset.url;
const domains = settings.customDomains || [];
if (domains.includes(domain)) return;
try {
const granted = await api.permissions.request({ origins: [domain + '/*'] });
if (!granted) return;
} catch (e) { /* non-fatal */ }
domains.push(domain);
settings.customDomains = domains;
await Storage.saveSettings({ customDomains: domains });
renderPopupDomains();
renderPopupDomainSuggestions();
});
});
}
// --- Util ---
function escapeHtml(str) {
const div = document.createElement('div');