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, "manifest_version": 3,
"name": "Silent Send", "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.", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Silent Send", "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.", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.",
"permissions": [ "permissions": [
"storage", "storage",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "silent-send", "name": "silent-send",
"version": "0.9.20", "version": "0.9.21",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"description": "Browser extension that substitutes personal data before sending to AI services", "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 // Built-in URL patterns
const BUILTIN_URL_PATTERNS = [ const BUILTIN_URL_PATTERNS = [
// AI services
'https://claude.ai/*', 'https://claude.ai/*',
'https://chatgpt.com/*', 'https://chatgpt.com/*',
'https://chat.openai.com/*', 'https://chat.openai.com/*',
@@ -29,19 +28,6 @@ const BUILTIN_URL_PATTERNS = [
'https://grok.x.ai/*', 'https://grok.x.ai/*',
'https://x.com/i/grok*', 'https://x.com/i/grok*',
'https://gemini.google.com/*', '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://localhost/*',
'http://127.0.0.1/*', 'http://127.0.0.1/*',
]; ];
@@ -76,44 +62,6 @@ api.tabs.onRemoved.addListener((tabId) => {
// --- Dynamic injection for custom domains --- // --- 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) { async function injectOnCustomDomain(tabId, tabUrl) {
const settings = await Storage.getSettings(); const settings = await Storage.getSettings();
const customDomains = settings.customDomains || []; const customDomains = settings.customDomains || [];
@@ -164,17 +112,6 @@ api.runtime.onMessage.addListener((message, sender, sendResponse) => {
}); });
const messageHandlers = { 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) { async 'substitution:performed'(message, sender) {
const tabId = sender.tab?.id; const tabId = sender.tab?.id;
if (tabId == null) return; if (tabId == null) return;
@@ -437,11 +374,6 @@ api.storage.onChanged.addListener(async (changes, areaName) => {
const settings = await Storage.getSettings(); const settings = await Storage.getSettings();
await updateIcon(settings); 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) // Push to browser.storage.sync when local data changes (same-browser cross-device)
if (areaName === 'local' && settings.browserSync) { if (areaName === 'local' && settings.browserSync) {
await SilentSendSync.pushToSyncStorage(); await SilentSendSync.pushToSyncStorage();
@@ -567,7 +499,6 @@ api.runtime.onInstalled.addListener(async () => {
api.action.setBadgeBackgroundColor({ color: '#6b7280' }); api.action.setBadgeBackgroundColor({ color: '#6b7280' });
const settings = await Storage.getSettings(); const settings = await Storage.getSettings();
await updateIcon(settings); await updateIcon(settings);
await syncCustomDomainScripts();
// Set up alarms on install // Set up alarms on install
await setupAutoSyncAlarm(); await setupAutoSyncAlarm();
await setupOrgPolicyAlarm(); await setupOrgPolicyAlarm();
@@ -577,7 +508,6 @@ api.runtime.onInstalled.addListener(async () => {
(async () => { (async () => {
const settings = await Storage.getSettings(); const settings = await Storage.getSettings();
await updateIcon(settings); await updateIcon(settings);
await syncCustomDomainScripts();
// Check if extension is locked (encrypted data, no cached key) // Check if extension is locked (encrypted data, no cached key)
const locked = await Storage.isLocked(); const locked = await Storage.isLocked();
+2 -127
View File
@@ -35,7 +35,7 @@
border-radius: 2px; border-radius: 2px;
} }
/* Auto-detect PII warning banner */ /* Auto-detect PPI warning banner */
.ss-autodetect-warning { .ss-autodetect-warning {
position: fixed; position: fixed;
top: 16px; top: 16px;
@@ -136,7 +136,7 @@
font-style: italic; font-style: italic;
} }
/* Pre-send PII warning (spellcheck-style, appears while typing) */ /* Pre-send PPI warning (spellcheck-style, appears while typing) */
.ss-presend-warning { .ss-presend-warning {
position: fixed; position: fixed;
top: 16px; top: 16px;
@@ -220,21 +220,6 @@
.ss-ps-add:hover { background: rgba(74, 222, 128, 0.15); } .ss-ps-add:hover { background: rgba(74, 222, 128, 0.15); }
.ss-ps-add:disabled { border-color: #333; cursor: default; } .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 */ /* Floating reveal mode indicator */
.ss-reveal-badge { .ss-reveal-badge {
position: fixed; position: fixed;
@@ -259,113 +244,3 @@
opacity: 1; opacity: 1;
transform: translateY(0); 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; }
+104 -766
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 // Load mappings and settings, then inject into page
async function init() { async function init() {
let mappings, identity, settings;
const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']); const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']);
const isEncrypted = result.ss_mappings?._ssLocalEncrypted || const 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; result.ss_identity?._ssLocalEncrypted;
const mappings = isLocked ? [] : (result.ss_mappings || []);
const identityData = isLocked ? {} : (result.ss_identity || {});
if (isEncrypted) { // Merge active profiles into a flat identity object for the content script
// Data is encrypted — ask the background script for decrypted config. const identity = mergeProfiles(identityData);
// 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 };
}
// 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);
// Inject the main interception script into the page's world // Inject the main interception script into the page's world
const script = document.createElement('script'); const script = document.createElement('script');
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
script.src = api.runtime.getURL('src/content/content.js'); script.src = api.runtime.getURL('src/content/content.js');
(document.head || document.documentElement).appendChild(script); (document.head || document.documentElement).appendChild(script);
script.onload = () => script.remove(); script.onload = () => script.remove();
+15 -93
View File
@@ -1,14 +1,14 @@
/** /**
* Silent Send - Auto-Detect * Silent Send - Auto-Detect
* *
* Scans text for potential PII that the user hasn't configured. * Scans text for potential PPI that the user hasn't configured.
* This catches things the identity and auto-redact scanner can't — * This catches things the identity and secret scanner can't —
* because the user forgot or didn't know to configure them. * because the user forgot or didn't know to configure them.
* *
* Returns warnings (not auto-redactions) so the user can decide. * Returns warnings (not auto-redactions) so the user can decide.
*/ */
const PII_PATTERNS = [ const PPI_PATTERNS = [
// --- Network --- // --- Network ---
{ {
name: 'Private IP Address', 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, 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', category: 'network',
hint: 'IP address — could identify your 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)$/, 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 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 = { 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. * Pass in identity so we can skip values the user already configured.
* *
* Returns array of { name, value, hint, category, index } * 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 // Skip context-dependent patterns if no context words present
if (pattern.contextRequired && !hasContext) continue; if (pattern.contextRequired && !hasContext) continue;
@@ -221,15 +221,16 @@ const AutoDetect = {
*/ */
_detectProperNouns(text, configured) { _detectProperNouns(text, configured) {
const findings = []; const findings = [];
// Only match TWO OR MORE consecutive capitalized words const re = /(?:^|[.!?\n]\s*)?([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*)/g;
// 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;
let m; let m;
while ((m = re.exec(text)) !== null) { while ((m = re.exec(text)) !== null) {
const fullMatch = m[1]; const fullMatch = m[1];
if (!fullMatch) continue; 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 words = fullMatch.split(/\s+/);
const properWords = words.filter(w => const properWords = words.filter(w =>
w.length >= 3 && w.length >= 3 &&
@@ -237,14 +238,15 @@ const AutoDetect = {
!configured.has(w.toLowerCase()) !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(' '); const value = properWords.join(' ');
if (value.length >= 5 && !configured.has(value.toLowerCase())) { if (value.length >= 3 && !configured.has(value.toLowerCase())) {
findings.push({ findings.push({
name: 'Possible Name/Org', name: 'Possible Name/Org',
value, value,
hint: 'Capitalized phrase — could be a name, company, or project', hint: 'Capitalized word — could be a name, company, or project',
category: 'name', category: 'name',
}); });
} }
@@ -260,8 +262,6 @@ const AutoDetect = {
}; };
// Common English words to exclude from proper noun detection // 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([ const COMMON_WORDS = new Set([
'the', 'and', 'but', 'for', 'not', 'you', 'all', 'can', 'had', 'her', 'the', 'and', 'but', 'for', 'not', 'you', 'all', 'can', 'had', 'her',
'was', 'one', 'our', 'out', 'are', 'has', 'his', 'how', 'its', 'may', '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', 'number', 'other', 'point', 'right', 'small', 'state', 'thing',
'think', 'those', 'three', 'through', 'under', 'until', 'water', 'think', 'those', 'three', 'through', 'under', 'until', 'water',
'world', 'write', 'might', 'should', 'because', 'although', '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 // Programming / tech terms
'string', 'number', 'boolean', 'object', 'array', 'function', 'class', 'string', 'number', 'boolean', 'object', 'array', 'function', 'class',
'type', 'error', 'null', 'undefined', 'true', 'false', 'return', 'type', 'error', 'null', 'undefined', 'true', 'false', 'return',
+5 -5
View File
@@ -12,7 +12,7 @@
* - Policy updates are applied automatically * - Policy updates are applied automatically
* *
* Privacy: the org admin can check compliance (are required fields * 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'; 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(); const policy = await this.getPolicy();
if (!policy?.requiredSecretPatterns?.length) return []; if (!policy?.requiredSecretPatterns?.length) return [];
@@ -220,7 +220,7 @@ const OrgPolicy = {
/** /**
* Check if the user's configuration meets org policy requirements. * 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[] }} * @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 * Detects common secret/credential patterns in text and either
* warns or auto-redacts them. This catches things the identity-based * warns or auto-redacts them. This catches things the identity-based
* smart patterns can't: API keys, tokens, passwords, SSNs, credit * smart patterns can't: API keys, tokens, passwords, SSNs, credit
* cards, private keys, connection strings, etc. * cards, private keys, connection strings, etc.
* *
* Supports user-defined custom patterns for proprietary token formats.
*
* Each pattern has: * Each pattern has:
* - name: human-readable label * - name: human-readable label
* - regex: detection pattern * - regex: detection pattern
@@ -15,7 +13,7 @@
* - severity: 'critical' (always redact) or 'warning' (flag but allow) * - severity: 'critical' (always redact) or 'warning' (flag but allow)
*/ */
const REDACT_PATTERNS = [ const SECRET_PATTERNS = [
// --- API Keys --- // --- API Keys ---
{ {
name: 'OpenAI API Key', name: 'OpenAI API Key',
@@ -163,39 +161,14 @@ const REDACT_PATTERNS = [
}, },
]; ];
const AutoRedact = { const SecretScanner = {
/**
* 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. * 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 findings = [];
const patterns = this._buildPatterns(customPatterns);
for (const pattern of patterns) { for (const pattern of SECRET_PATTERNS) {
// Reset regex lastIndex // Reset regex lastIndex
pattern.regex.lastIndex = 0; pattern.regex.lastIndex = 0;
let match; let match;
@@ -231,11 +204,9 @@ const AutoRedact = {
/** /**
* Redact all critical secrets in text. Warnings are not auto-redacted. * Redact all critical secrets in text. Warnings are not auto-redacted.
* Returns { text, redactions[] } * Returns { text, redactions[] }
* @param {string} text
* @param {Array} [customPatterns] from settings.customRedactPatterns
*/ */
redact(text, customPatterns) { redact(text) {
const findings = this.scan(text, customPatterns); const findings = this.scan(text);
const redactions = []; const redactions = [];
let result = text; let result = text;
@@ -249,7 +220,7 @@ const AutoRedact = {
redactions.push({ redactions.push({
original: f.value, original: f.value,
replaced: f.redactTo, replaced: f.redactTo,
category: 'redact', category: 'secret',
pattern: f.name, pattern: f.name,
}); });
} }
@@ -266,7 +237,7 @@ const AutoRedact = {
}; };
if (typeof globalThis !== 'undefined') { 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', 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 // All user data keys are encrypted at rest — settings included since
// custom domains and configuration can reveal what services the user // custom domains and configuration can reveal what services the user
// accesses. Only ss_sync_encryption (salt, verification blob) and // accesses. Only ss_sync_encryption (salt, verification blob) and
@@ -34,13 +34,12 @@ const DEFAULT_SETTINGS = {
enabled: true, enabled: true,
showHighlights: false, showHighlights: false,
revealMode: false, revealMode: false,
autoRedact: true, secretScanning: true,
autoDetect: true, autoDetect: true,
autoRedactDetected: true, autoRedactDetected: true,
autoAddDetected: true, autoAddDetected: true,
maxLogEntries: 100, maxLogEntries: 200,
customDomains: [], customDomains: [],
customRedactPatterns: [],
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'], categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
browserSync: false, browserSync: false,
}; };
+11 -24
View File
@@ -12,9 +12,8 @@
* 5. Custom HTTP endpoint — any URL supporting GET + PUT (WebDAV, * 5. Custom HTTP endpoint — any URL supporting GET + PUT (WebDAV,
* self-hosted server, cloud function, etc.). * self-hosted server, cloud function, etc.).
* *
* Encryption: all sync channels REQUIRE encryption with a password * Encryption: all sync channels can optionally encrypt data with a
* (AES-256-GCM) and/or TOTP verification. Syncing without encryption * password (AES-256-GCM) and/or require TOTP verification.
* is not permitted — users must set up encryption before enabling sync.
* Authentication is cached with a configurable TTL so the user only * Authentication is cached with a configurable TTL so the user only
* needs to authenticate when the cache expires and new data exists. * needs to authenticate when the cache expires and new data exists.
* *
@@ -371,9 +370,6 @@ const SilentSendSync = {
const StorageModule = (await import('./storage.js')).default; const StorageModule = (await import('./storage.js')).default;
await StorageModule.decryptAllData(); 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 api.storage.local.remove('ss_sync_encryption');
await SilentSendCrypto.clearCachedKey(); await SilentSendCrypto.clearCachedKey();
await SilentSendCrypto.clearWebAuthnCredential(); await SilentSendCrypto.clearWebAuthnCredential();
@@ -419,7 +415,7 @@ const SilentSendSync = {
*/ */
async _encryptForSync(data) { async _encryptForSync(data) {
const config = await this._getSyncEncryption(); 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(); const keyInfo = await this._getEncryptionKey();
if (!keyInfo) { if (!keyInfo) {
@@ -583,15 +579,12 @@ const SilentSendSync = {
async exportSyncCode() { async exportSyncCode() {
const data = await this._getAllData(); const data = await this._getAllData();
// Encrypt (mandatory) // Encrypt if enabled
const result = await this._encryptForSync(data); const result = await this._encryptForSync(data);
if (result.needsEncryption) {
return { needsEncryption: true };
}
if (result.needsAuth) { if (result.needsAuth) {
return { needsAuth: true }; return { needsAuth: true };
} }
const payload = result.data; const payload = result.data || data;
const json = JSON.stringify(payload); const json = JSON.stringify(payload);
return btoa(unescape(encodeURIComponent(json))); return btoa(unescape(encodeURIComponent(json)));
@@ -647,10 +640,10 @@ const SilentSendSync = {
try { try {
const data = await this._getAllData(); const data = await this._getAllData();
// Encrypt (mandatory) // Encrypt if enabled
const result = await this._encryptForSync(data); const result = await this._encryptForSync(data);
if (result.needsEncryption || result.needsAuth) return; // skip — encryption required if (result.needsAuth) return; // silently skip — will sync on next auth
const payload = result.data; const payload = result.data || data;
const json = JSON.stringify(payload); const json = JSON.stringify(payload);
@@ -716,15 +709,12 @@ const SilentSendSync = {
try { try {
const data = await this._getAllData(); const data = await this._getAllData();
// Encrypt (mandatory) // Encrypt if enabled
const encResult = await this._encryptForSync(data); const encResult = await this._encryptForSync(data);
if (encResult.needsEncryption) {
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
}
if (encResult.needsAuth) { if (encResult.needsAuth) {
return { success: false, needsAuth: true, reason: 'Authentication required.' }; 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 content = JSON.stringify(payload, null, 2);
const stored = await api.storage.local.get('ss_gist_id'); const stored = await api.storage.local.get('ss_gist_id');
@@ -820,13 +810,10 @@ const SilentSendSync = {
const data = await this._getAllData(); const data = await this._getAllData();
const encResult = await this._encryptForSync(data); const encResult = await this._encryptForSync(data);
if (encResult.needsEncryption) {
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
}
if (encResult.needsAuth) { if (encResult.needsAuth) {
return { success: false, needsAuth: true, reason: 'Authentication required.' }; return { success: false, needsAuth: true, reason: 'Authentication required.' };
} }
const payload = encResult.data; const payload = encResult.data || data;
const resp = await fetch(url, { const resp = await fetch(url, {
method, method,
+16 -63
View File
@@ -49,38 +49,17 @@
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div> <div>
<label>Auto Redact</label> <label>Secret scanning</label>
<p class="setting-desc">Automatically detect and redact API keys, tokens, passwords, SSNs, credit card numbers, and custom patterns</p> <p class="setting-desc">Auto-detect and redact API keys, tokens, passwords, SSNs, credit card numbers</p>
</div> </div>
<label class="toggle"> <label class="toggle">
<input type="checkbox" id="autoRedactToggle" checked> <input type="checkbox" id="secretScanning" checked>
<span class="toggle-slider"></span> <span class="toggle-slider"></span>
</label> </label>
</div> </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 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> <p class="setting-desc">Warn when potential personal data (IPs, addresses, paths) is detected that you haven't configured</p>
</div> </div>
<label class="toggle"> <label class="toggle">
@@ -90,8 +69,8 @@
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div> <div>
<label>Auto-redact detected PII on send</label> <label>Auto-redact detected PPI 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> <p class="setting-desc">Automatically replace detected PPI with generic placeholders (192.0.2.1, 123 Example Street, etc.) when sending</p>
</div> </div>
<label class="toggle"> <label class="toggle">
<input type="checkbox" id="autoRedactDetected" checked> <input type="checkbox" id="autoRedactDetected" checked>
@@ -100,8 +79,8 @@
</div> </div>
<div class="setting-row"> <div class="setting-row">
<div> <div>
<label>Offer to auto-add detected PII</label> <label>Offer to auto-add detected PPI</label>
<p class="setting-desc">Show a + button on detected PII to instantly create a mapping with a suggested fake value</p> <p class="setting-desc">Show a + button on detected PPI to instantly create a mapping with a suggested fake value</p>
</div> </div>
<label class="toggle"> <label class="toggle">
<input type="checkbox" id="autoAddDetected" checked> <input type="checkbox" id="autoAddDetected" checked>
@@ -113,7 +92,7 @@
<label>Max log entries</label> <label>Max log entries</label>
<p class="setting-desc">Number of activity log entries to keep</p> <p class="setting-desc">Number of activity log entries to keep</p>
</div> </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> </div>
</section> </section>
@@ -127,7 +106,7 @@
<span>&#128274;</span> Sync Encryption <span>&#128274;</span> Sync Encryption
</h3> </h3>
<p class="section-desc" style="margin-bottom:10px"> <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> </p>
<div id="syncEncryptionSetup"> <div id="syncEncryptionSetup">
@@ -382,7 +361,7 @@
<!-- Organization --> <!-- Organization -->
<section class="section"> <section class="section">
<h2>Organization</h2> <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 id="orgNotJoined">
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px"> <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
@@ -542,31 +521,12 @@
<h2>Custom Domains</h2> <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> <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="domain-list" id="domainList"></div>
<div class="add-row" style="flex-wrap:wrap;gap:8px"> <div class="add-row">
<input type="text" id="newDomain" placeholder="https://ai.myserver.com" class="input" style="flex:2;min-width:200px"> <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 btn-primary" id="btnAddDomain">Add Domain</button>
<button class="btn" id="btnBulkAddDomains">Bulk Add</button>
</div> </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"> <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> </p>
</section> </section>
@@ -631,14 +591,7 @@
</section> </section>
<footer> <footer>
<p>Silent Send v0.9.20</p> <p>Silent Send v0.3.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> </footer>
</div> </div>
+57 -340
View File
@@ -14,23 +14,17 @@ let passwordsRevealed = false;
const $ = (sel) => document.querySelector(sel); 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 () => { document.addEventListener('DOMContentLoaded', async () => {
mappings = await Storage.getMappings(); mappings = await Storage.getMappings();
settings = await Storage.getSettings(); settings = await Storage.getSettings();
// Apply settings to UI // Apply settings to UI
$('#showHighlights').checked = settings.showHighlights || false; $('#showHighlights').checked = settings.showHighlights || false;
$('#autoRedactToggle').checked = settings.autoRedact !== false; $('#secretScanning').checked = settings.secretScanning !== false;
$('#autoDetect').checked = settings.autoDetect !== false; $('#autoDetect').checked = settings.autoDetect !== false;
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false; $('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
$('#autoAddDetected').checked = settings.autoAddDetected !== false; $('#autoAddDetected').checked = settings.autoAddDetected !== false;
$('#maxLogEntries').value = settings.maxLogEntries || 100; $('#maxLogEntries').value = settings.maxLogEntries || 200;
$('#browserSync').checked = settings.browserSync === true; $('#browserSync').checked = settings.browserSync === true;
renderMappings(); renderMappings();
@@ -51,28 +45,17 @@ document.addEventListener('DOMContentLoaded', async () => {
// --- Sync section --- // --- Sync section ---
$('#browserSync').addEventListener('change', async (e) => { $('#browserSync').addEventListener('change', async (e) => {
await Storage.saveSettings({ browserSync: e.target.checked });
if (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(); await SilentSendSync.pushToSyncStorage();
setSyncStatus('Browser account sync enabled. Your settings will sync automatically.', 'ok'); setSyncStatus('Browser account sync enabled. Your settings will sync automatically.', 'ok');
} else { } else {
await Storage.saveSettings({ browserSync: false });
setSyncStatus('Browser account sync disabled.', 'neutral'); setSyncStatus('Browser account sync disabled.', 'neutral');
} }
}); });
$('#btnGenerateSyncCode').addEventListener('click', async () => { $('#btnGenerateSyncCode').addEventListener('click', async () => {
const code = await SilentSendSync.exportSyncCode(); const code = await SilentSendSync.exportSyncCode();
if (code?.needsEncryption) {
setSyncStatus('Encryption must be enabled before syncing. Set up encryption first.', 'error');
return;
}
if (code?.needsAuth) { if (code?.needsAuth) {
setSyncStatus('Authentication required to encrypt sync code.', 'warn'); setSyncStatus('Authentication required to encrypt sync code.', 'warn');
showSyncAuthPrompt(); showSyncAuthPrompt();
@@ -185,9 +168,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (!token) { setGistSyncStatus('Enter your GitHub PAT first.', 'warn'); return; } if (!token) { setGistSyncStatus('Enter your GitHub PAT first.', 'warn'); return; }
setGistSyncStatus('Pushing…', 'neutral'); setGistSyncStatus('Pushing…', 'neutral');
const r = await SilentSendSync.pushToGist(token); const r = await SilentSendSync.pushToGist(token);
if (r.needsEncryption) { if (r.needsAuth) {
setGistSyncStatus('Encryption must be enabled before syncing.', 'error');
} else if (r.needsAuth) {
setGistSyncStatus('Authentication required to encrypt.', 'warn'); setGistSyncStatus('Authentication required to encrypt.', 'warn');
showSyncAuthPrompt(); showSyncAuthPrompt();
} else if (r.success) { } else if (r.success) {
@@ -226,9 +207,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const headers = parseHeadersField($('#customSyncHeaders').value); const headers = parseHeadersField($('#customSyncHeaders').value);
setUrlSyncStatus('Pushing…', 'neutral'); setUrlSyncStatus('Pushing…', 'neutral');
const r = await SilentSendSync.pushToUrl({ url, headers }); const r = await SilentSendSync.pushToUrl({ url, headers });
if (r.needsEncryption) { if (r.needsAuth) {
setUrlSyncStatus('Encryption must be enabled before syncing.', 'error');
} else if (r.needsAuth) {
setUrlSyncStatus('Authentication required to encrypt.', 'warn'); setUrlSyncStatus('Authentication required to encrypt.', 'warn');
showSyncAuthPrompt(); showSyncAuthPrompt();
} else if (r.success) { } else if (r.success) {
@@ -280,34 +259,14 @@ document.addEventListener('DOMContentLoaded', async () => {
$('#newDomain').addEventListener('keydown', (e) => { $('#newDomain').addEventListener('keydown', (e) => {
if (e.key === 'Enter') addDomain(); 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 // Settings listeners
$('#showHighlights').addEventListener('change', async (e) => { $('#showHighlights').addEventListener('change', async (e) => {
await Storage.saveSettings({ showHighlights: e.target.checked }); await Storage.saveSettings({ showHighlights: e.target.checked });
}); });
$('#autoRedactToggle').addEventListener('change', async (e) => { $('#secretScanning').addEventListener('change', async (e) => {
await Storage.saveSettings({ autoRedact: e.target.checked }); await Storage.saveSettings({ secretScanning: 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) => { $('#autoDetect').addEventListener('change', async (e) => {
@@ -323,7 +282,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
$('#maxLogEntries').addEventListener('change', async (e) => { $('#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 // Add mapping
@@ -452,11 +411,11 @@ function renderMappings() {
const nonPasswordMappings = mappings.filter(m => m.category !== 'password'); const nonPasswordMappings = mappings.filter(m => m.category !== 'password');
if (nonPasswordMappings.length === 0) { 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; return;
} }
safeHTML(tbody, nonPasswordMappings tbody.innerHTML = nonPasswordMappings
.map( .map(
(m) => ` (m) => `
<tr data-id="${m.id}"> <tr data-id="${m.id}">
@@ -474,7 +433,7 @@ function renderMappings() {
</tr> </tr>
` `
) )
.join('')); .join('');
// Bind // Bind
tbody.querySelectorAll('.btn-delete').forEach((btn) => { tbody.querySelectorAll('.btn-delete').forEach((btn) => {
@@ -504,14 +463,14 @@ function renderPasswords() {
const noMsg = $('#noPasswordsMsg'); const noMsg = $('#noPasswordsMsg');
if (passwordMappings.length === 0) { if (passwordMappings.length === 0) {
tbody.replaceChildren(); tbody.innerHTML = '';
noMsg.style.display = 'block'; noMsg.style.display = 'block';
return; return;
} }
noMsg.style.display = 'none'; noMsg.style.display = 'none';
safeHTML(tbody, passwordMappings.map(m => { tbody.innerHTML = passwordMappings.map(m => {
const displayReal = passwordsRevealed const displayReal = passwordsRevealed
? escapeHtml(m.real) ? escapeHtml(m.real)
: '&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;'; : '&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;';
@@ -528,7 +487,7 @@ function renderPasswords() {
</td> </td>
<td><button class="btn btn-sm btn-danger btn-delete-pw">&times;</button></td> <td><button class="btn btn-sm btn-danger btn-delete-pw">&times;</button></td>
</tr>`; </tr>`;
}).join('')); }).join('');
// Bind delete // Bind delete
tbody.querySelectorAll('.btn-delete-pw').forEach(btn => { tbody.querySelectorAll('.btn-delete-pw').forEach(btn => {
@@ -565,11 +524,11 @@ async function renderLog() {
const list = $('#logList'); const list = $('#logList');
if (log.length === 0) { 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; return;
} }
safeHTML(list, log list.innerHTML = log
.slice(0, 100) .slice(0, 100)
.map((entry) => { .map((entry) => {
const time = new Date(entry.timestamp).toLocaleString(); const time = new Date(entry.timestamp).toLocaleString();
@@ -582,315 +541,74 @@ async function renderLog() {
</div> </div>
`; `;
}) })
.join('')); .join('');
} }
// --- Custom Domains --- // --- Custom Domains ---
async function addDomain() {
let domain = $('#newDomain').value.trim();
if (!domain) return;
// Suggested popular domains (not already built-in) // Normalize: ensure it has a protocol
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;
if (!domain.startsWith('http://') && !domain.startsWith('https://')) { if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
domain = 'https://' + domain; domain = 'https://' + domain;
} }
return domain.replace(/\/+$/, ''); // Strip trailing slashes
} domain = domain.replace(/\/+$/, '');
async function addSingleDomain(domain) {
const domains = settings.customDomains || []; 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 { try {
const granted = await api.permissions.request({ origins: [domain + '/*'] }); const granted = await api.permissions.request({
if (!granted) return { added: false, reason: 'denied' }; origins: [domain + '/*'],
});
if (!granted) {
alert('Permission denied. The extension needs access to this domain to work.');
return;
}
} catch (e) { } catch (e) {
// Firefox or older Chrome may not support optional permissions this way
console.warn('[Silent Send] Could not request permission:', e); console.warn('[Silent Send] Could not request permission:', e);
} }
domains.push(domain); domains.push(domain);
settings.customDomains = domains; settings.customDomains = domains;
await Storage.saveSettings({ 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(); renderDomains();
renderSuggestedDomains();
$('#newDomain').value = ''; $('#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() { function renderDomains() {
const list = $('#domainList'); const list = $('#domainList');
const domains = settings.customDomains || []; const domains = settings.customDomains || [];
if (domains.length === 0) { 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; return;
} }
safeHTML(list, domains list.innerHTML = domains
.map((d, i) => ` .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"> <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> <span style="font-size:13px;font-family:monospace">${escapeHtml(d)}</span>
<div style="display:flex;gap:4px;margin-left:8px"> <button class="btn btn-sm btn-danger btn-remove-domain" data-index="${i}">&times;</button>
<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>
</div> </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) => { list.querySelectorAll('.btn-remove-domain').forEach((btn) => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
const idx = parseInt(btn.dataset.index, 10); const idx = parseInt(btn.dataset.index, 10);
const domains = settings.customDomains || []; const domains = settings.customDomains || [];
const removed = domains.splice(idx, 1)[0]; domains.splice(idx, 1);
settings.customDomains = domains; settings.customDomains = domains;
await Storage.saveSettings({ customDomains: domains }); await Storage.saveSettings({ customDomains: domains });
if (removed) {
try {
await api.permissions.remove({ origins: [removed + '/*'] });
} catch (e) { /* non-fatal */ }
}
renderDomains(); 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 () => { $('#btnDisableEncryption').addEventListener('click', async () => {
if (!window.confirm('Disable sync encryption? Existing encrypted sync data will become unreadable.')) return; if (!window.confirm('Disable sync encryption? Existing encrypted sync data will become unreadable.')) return;
await SilentSendSync.disableEncryption(); await SilentSendSync.disableEncryption();
$('#browserSync').checked = false;
showEncryptionNotConfigured(); showEncryptionNotConfigured();
setSyncEncStatus('Encryption disabled. All sync channels have been turned off.', 'neutral'); setSyncEncStatus('Encryption disabled.', 'neutral');
}); });
// Change password // Change password
@@ -1578,11 +1295,11 @@ async function renderVersionHistory() {
const snapshots = await VersionHistory.getSnapshots(); const snapshots = await VersionHistory.getSnapshots();
if (snapshots.length === 0) { 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; return;
} }
safeHTML(list, snapshots.map(s => { list.innerHTML = snapshots.map(s => {
const time = new Date(s.timestamp).toLocaleString(); const time = new Date(s.timestamp).toLocaleString();
const mappingCount = (s.data?.mappings || []).length; 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"> 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> </div>
<button class="btn btn-sm btn-restore-snapshot" data-id="${s.id}">Restore</button> <button class="btn btn-sm btn-restore-snapshot" data-id="${s.id}">Restore</button>
</div>`; </div>`;
}).join('')); }).join('');
list.querySelectorAll('.btn-restore-snapshot').forEach(btn => { list.querySelectorAll('.btn-restore-snapshot').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
@@ -1638,13 +1355,13 @@ async function renderDevices() {
const entries = Object.values(devices); const entries = Object.values(devices);
if (entries.length === 0) { 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; return;
} }
entries.sort((a, b) => (b.lastSync || 0) - (a.lastSync || 0)); 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"> <thead><tr style="text-align:left;border-bottom:1px solid #e5e7eb">
<th style="padding:6px">Device</th> <th style="padding:6px">Device</th>
<th style="padding:6px">Browser</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> <td style="padding:6px">${!isCurrent ? `<button class="btn btn-sm btn-danger btn-remove-device" data-id="${d.id}">&times;</button>` : ''}</td>
</tr>`; </tr>`;
}).join('')}</tbody> }).join('')}</tbody>
</table>`); </table>`;
list.querySelectorAll('.btn-remove-device').forEach(btn => { list.querySelectorAll('.btn-remove-device').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
@@ -1738,9 +1455,9 @@ async function showOrgJoined() {
const compliance = await OrgPolicy.checkCompliance(); const compliance = await OrgPolicy.checkCompliance();
const statusEl = $('#orgComplianceStatus'); const statusEl = $('#orgComplianceStatus');
if (compliance.compliant) { 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 { } 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 || []; const reqMappings = policy?.requiredMappings || [];
@@ -1892,7 +1609,7 @@ async function checkConflicts() {
function renderConflicts(conflicts) { function renderConflicts(conflicts) {
const list = $('#conflictList'); 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="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="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"> <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> <button class="btn btn-sm btn-resolve" data-id="${c.id}" data-choice="remote">Keep Remote</button>
</div> </div>
</div> </div>
`).join('')); `).join('');
list.querySelectorAll('.btn-resolve').forEach(btn => { list.querySelectorAll('.btn-resolve').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
@@ -1971,10 +1688,10 @@ async function handleBulkImport(e) {
result.identity.usernames.filter(u => !u.substitute).length + result.identity.usernames.filter(u => !u.substitute).length +
result.identity.phones.filter(p => !p.substitute).length; result.identity.phones.filter(p => !p.substitute).length;
safeHTML($('#bulkImportSummary'), ` $('#bulkImportSummary').innerHTML = `
Found: ${parts.join(', ')}. Found: ${parts.join(', ')}.
${needsMapping > 0 ? `<span style="color:#b45309">${needsMapping} item(s) need substitutes — you can add them after import.</span>` : ''} ${needsMapping > 0 ? `<span style="color:#b45309">${needsMapping} item(s) need substitutes — you can add them after import.</span>` : ''}
`); `;
// Build preview list // Build preview list
const items = []; 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>`); 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('') + $('#bulkImportItems').innerHTML = items.slice(0, 50).join('') +
(items.length > 50 ? `<div style="color:#6b7280;margin-top:4px">+${items.length - 50} more...</div>` : '')); (items.length > 50 ? `<div style="color:#6b7280;margin-top:4px">+${items.length - 50} more...</div>` : '');
$('#bulkImportPreview').style.display = 'block'; $('#bulkImportPreview').style.display = 'block';
-29
View File
@@ -623,35 +623,6 @@ body {
font-size: 12px; 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 */
.footer { .footer {
padding: 8px 16px; padding: 8px 16px;
+4 -77
View File
@@ -33,7 +33,6 @@
<button class="tab" data-tab="mappings">Mappings</button> <button class="tab" data-tab="mappings">Mappings</button>
<button class="tab" data-tab="activity">Activity</button> <button class="tab" data-tab="activity">Activity</button>
<button class="tab" data-tab="test">Test</button> <button class="tab" data-tab="test">Test</button>
<button class="tab" data-tab="options">Options</button>
</nav> </nav>
<!-- Locked State Overlay --> <!-- Locked State Overlay -->
@@ -194,75 +193,6 @@
<div class="test-identity-status" id="identityStatus"></div> <div class="test-identity-status" id="identityStatus"></div>
</section> </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 -->
<footer class="footer"> <footer class="footer">
<div class="privacy-note"> <div class="privacy-note">
@@ -271,14 +201,11 @@
AES-256 encrypted at rest — unreadable without your password.</span> AES-256 encrypted at rest — unreadable without your password.</span>
</div> </div>
<div class="privacy-note" style="color:#b45309;background:#fef3c7;padding:6px 8px;border-radius:4px;margin-bottom:6px"> <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 Silent Send is a convenience tool, not a security guarantee. It can miss
but cannot eliminate the risk of sharing personal data. It may miss PII PPI in images, file uploads, unusual name forms, or data you forgot to
in images, unusual formats, or data you haven't configured. Third-party configure. Always verify sensitive messages before sending.
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> </div>
<a href="#" id="btnOptions">Options</a>
</footer> </footer>
</div> </div>
+41 -196
View File
@@ -1,6 +1,6 @@
import SubstitutionEngine from '../lib/substitution-engine.js'; import SubstitutionEngine from '../lib/substitution-engine.js';
import SmartPatterns from '../lib/smart-patterns.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 AutoDetect from '../lib/auto-detect.js';
import Storage from '../lib/storage.js'; import Storage from '../lib/storage.js';
import SilentSendSync from '../lib/sync.js'; import SilentSendSync from '../lib/sync.js';
@@ -18,12 +18,6 @@ let settings = {};
const $ = (sel) => document.querySelector(sel); const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(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 --- // --- Init ---
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
// Check if locked BEFORE trying to read sensitive data // Check if locked BEFORE trying to read sensitive data
@@ -208,45 +202,12 @@ async function initUnlockedUI() {
}); });
}); });
// Open full options page button (Options tab) // Options link
$('#btnOpenFullOptions').addEventListener('click', () => { $('#btnOptions').addEventListener('click', (e) => {
e.preventDefault();
api.runtime.openOptionsPage(); 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 // Update privacy note based on encryption state
const encEnabled = await Storage._isAtRestEncryptionEnabled(); const encEnabled = await Storage._isAtRestEncryptionEnabled();
const encNote = $('#privacyEncNote'); const encNote = $('#privacyEncNote');
@@ -341,17 +302,11 @@ async function showLockedUI() {
// --- Profiles --- // --- Profiles ---
function renderProfileSelector() { function renderProfileSelector() {
const select = $('#profileSelect'); const select = $('#profileSelect');
// Build options via DOM API — safeHTML + DOMParser mangles <option> elements select.innerHTML = profiles.map(p =>
select.replaceChildren(); `<option value="${p.id}" ${p.id === currentProfileId ? 'selected' : ''}>` +
for (const p of profiles) { `${escapeHtml(p.name)}${p.active ? '' : ' (off)'}` +
const opt = new Option( `</option>`
`${p.name}${p.active ? '' : ' (off)'}`, ).join('');
p.id,
false,
p.id === currentProfileId
);
select.appendChild(opt);
}
const profile = profiles.find(p => p.id === currentProfileId); const profile = profiles.find(p => p.id === currentProfileId);
$('#profileActive').checked = profile?.active ?? true; $('#profileActive').checked = profile?.active ?? true;
@@ -400,7 +355,7 @@ function renderFieldList(fieldName, items) {
items = [{ real: '', substitute: '', type: config.defaultType || '' }]; items = [{ real: '', substitute: '', type: config.defaultType || '' }];
} }
safeHTML(container, items.map((item, i) => { container.innerHTML = items.map((item, i) => {
let typeHtml = ''; let typeHtml = '';
if (config.typeOptions) { 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">` + 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}"> <input type="text" class="input input-sm id-sub" value="${escapeAttr(item.substitute || '')}" placeholder="${config.placeholderSub}">
<button class="btn-remove" title="Remove">&times;</button> <button class="btn-remove" title="Remove">&times;</button>
</div>`; </div>`;
}).join('')); }).join('');
// Bind remove buttons // Bind remove buttons
container.querySelectorAll('.btn-remove').forEach(btn => { container.querySelectorAll('.btn-remove').forEach(btn => {
@@ -465,13 +420,13 @@ function loadIdentityForm() {
).join('') + ).join('') +
`</select>`; `</select>`;
} }
safeHTML(tempDiv, `<div class="id-entry-row" data-index="${count}"> tempDiv.innerHTML = `<div class="id-entry-row" data-index="${count}">
${typeHtml} ${typeHtml}
<input type="text" class="input input-sm id-real" placeholder="${config.placeholderReal}"> <input type="text" class="input input-sm id-real" placeholder="${config.placeholderReal}">
<span class="arrow" style="font-size:12px">&rarr;</span> <span class="arrow" style="font-size:12px">&rarr;</span>
<input type="text" class="input input-sm id-sub" placeholder="${config.placeholderSub}"> <input type="text" class="input input-sm id-sub" placeholder="${config.placeholderSub}">
<button class="btn-remove" title="Remove">&times;</button> <button class="btn-remove" title="Remove">&times;</button>
</div>`); </div>`;
const row = tempDiv.firstElementChild; const row = tempDiv.firstElementChild;
container.appendChild(row); container.appendChild(row);
row.querySelector('.btn-remove').addEventListener('click', () => { row.querySelector('.btn-remove').addEventListener('click', () => {
@@ -621,11 +576,11 @@ function renderMappings() {
const list = $('#mappingList'); const list = $('#mappingList');
if (mappings.length === 0) { 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; return;
} }
safeHTML(list, mappings list.innerHTML = mappings
.map( .map(
(m) => ` (m) => `
<div class="mapping-item" data-id="${m.id}"> <div class="mapping-item" data-id="${m.id}">
@@ -642,7 +597,7 @@ function renderMappings() {
</div> </div>
` `
) )
.join('')); .join('');
// Bind actions // Bind actions
list.querySelectorAll('.btn-delete').forEach((btn) => { list.querySelectorAll('.btn-delete').forEach((btn) => {
@@ -676,11 +631,11 @@ async function renderActivity() {
countEl.textContent = `${log.length} substitution${log.length !== 1 ? 's' : ''} logged`; countEl.textContent = `${log.length} substitution${log.length !== 1 ? 's' : ''} logged`;
if (log.length === 0) { 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; return;
} }
safeHTML(list, log list.innerHTML = log
.slice(0, 50) .slice(0, 50)
.map((entry) => { .map((entry) => {
const time = new Date(entry.timestamp).toLocaleTimeString([], { const time = new Date(entry.timestamp).toLocaleTimeString([], {
@@ -698,7 +653,7 @@ async function renderActivity() {
</div> </div>
`; `;
}) })
.join('')); .join('');
} }
// --- Test Diff (Strip: real → fake) --- // --- Test Diff (Strip: real → fake) ---
@@ -708,23 +663,23 @@ function renderTestDiff() {
const stats = $('#diffStats'); const stats = $('#diffStats');
if (!input) { if (!input) {
output.replaceChildren(); output.innerHTML = '';
stats.textContent = ''; stats.textContent = '';
return; return;
} }
const smartResult = SmartPatterns.substitute(input, identity); const smartResult = SmartPatterns.substitute(input, identity);
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings); const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
const redactResult = AutoRedact.redact(explicitResult.text, settings.customRedactPatterns); const secretResult = SecretScanner.redact(explicitResult.text);
const allReplacements = [ const allReplacements = [
...smartResult.replacements, ...smartResult.replacements,
...explicitResult.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; output.textContent = input;
stats.textContent = 'No substitutions detected'; stats.textContent = 'No substitutions detected';
return; return;
@@ -740,40 +695,38 @@ function renderTestDiff() {
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>` `<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
); );
} }
// Highlight auto-redactions in red // Highlight secret redactions in red
for (const r of redactResult.redactions) { for (const r of secretResult.redactions) {
const escapedReplaced = escapeHtml(r.replaced); const escapedReplaced = escapeHtml(r.replaced);
html = html.replace( html = html.replace(
escapedReplaced, escapedReplaced,
`<span class="sub-highlight" style="background:#fee2e2;color:#dc2626" title="${escapeHtml(r.pattern)}">${escapedReplaced}</span>` `<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 smartCount = smartResult.replacements.length;
const explicitCount = explicitResult.replacements.length; const explicitCount = explicitResult.replacements.length;
const redactCount = redactResult.redactions.length; const secretCount = secretResult.redactions.length;
const warnCount = redactResult.warnings.length; const warnCount = secretResult.warnings.length;
const parts = []; const parts = [];
if (smartCount > 0) parts.push(`${smartCount} smart`); if (smartCount > 0) parts.push(`${smartCount} smart`);
if (explicitCount > 0) parts.push(`${explicitCount} explicit`); 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`); if (warnCount > 0) parts.push(`${warnCount} warnings`);
// Auto-detect unconfigured PII in the final text // Auto-detect unconfigured PPI in the final text
const piiWarnings = AutoDetect.scan(finalText, identity); const ppiWarnings = AutoDetect.scan(finalText, identity);
if (piiWarnings.length > 0) parts.push(`${piiWarnings.length} PII detected`); if (ppiWarnings.length > 0) parts.push(`${ppiWarnings.length} PPI detected`);
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`; stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
// Show PII warnings below stats // Show PPI warnings below stats
if (piiWarnings.length > 0) { if (ppiWarnings.length > 0) {
const piiDiv = document.createElement('div'); stats.innerHTML += `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px">
safeHTML(piiDiv, `<div style="margin-top:6px;padding:6px 8px;background:#fef3c7;border-radius:4px;color:#92400e;font-size:11px"> <strong>Unconfigured PPI detected:</strong>
<strong>Unconfigured PII 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('')}
${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>`;
</div>`);
stats.appendChild(piiDiv);
} }
} }
@@ -784,7 +737,7 @@ function renderRevealDiff() {
const stats = $('#revealStats'); const stats = $('#revealStats');
if (!input) { if (!input) {
output.replaceChildren(); output.innerHTML = '';
stats.textContent = ''; stats.textContent = '';
return; return;
} }
@@ -830,7 +783,7 @@ function renderRevealDiff() {
`<span class="sub-highlight" title="Was: ${escapeHtml(pair.substitute)}" style="background:#dbeafe;color:#1d4ed8">${escapedReal}</span>` `<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`; stats.textContent = `${totalCount} value${totalCount !== 1 ? 's' : ''} revealed`;
} }
@@ -911,114 +864,6 @@ function updateStatusDot() {
dot.classList.toggle('disabled', !settings.enabled); 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 --- // --- Util ---
function escapeHtml(str) { function escapeHtml(str) {
const div = document.createElement('div'); const div = document.createElement('div');