Merge pull request #58 from outis1one/claude/read-repo-YMu21
revert: restore src/ from pre-doc-scanner baseline, bump 0.9.21
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+10
-45
@@ -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
@@ -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',
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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>🔒</span> Sync Encryption
|
<span>🔒</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 https://openwebui.local 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>
|
||||||
|
|
||||||
|
|||||||
+58
-341
@@ -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)
|
||||||
: '••••••••';
|
: '••••••••';
|
||||||
@@ -528,7 +487,7 @@ function renderPasswords() {
|
|||||||
</td>
|
</td>
|
||||||
<td><button class="btn btn-sm btn-danger btn-delete-pw">×</button></td>
|
<td><button class="btn btn-sm btn-danger btn-delete-pw">×</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(/\/+$/, '');
|
||||||
|
|
||||||
|
const domains = settings.customDomains || [];
|
||||||
|
if (domains.includes(domain)) {
|
||||||
|
alert('Domain already added.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addSingleDomain(domain) {
|
// Request browser permission for this domain
|
||||||
const domains = settings.customDomains || [];
|
|
||||||
if (domains.includes(domain)) return { added: false, reason: 'duplicate' };
|
|
||||||
|
|
||||||
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}">×</button>
|
||||||
<button class="btn btn-sm btn-edit-domain" data-index="${i}" title="Edit">✎</button>
|
|
||||||
<button class="btn btn-sm btn-danger btn-remove-domain" data-index="${i}" title="Remove">×</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">→ ${escapeHtml(p.redact)}</span>
|
|
||||||
<button class="btn btn-sm btn-danger btn-remove-redact" data-index="${i}" style="padding:2px 6px">×</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}">×</button>` : ''}</td>
|
<td style="padding:6px">${!isCurrent ? `<button class="btn btn-sm btn-danger btn-remove-device" data-id="${d.id}">×</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">✓ Compliant — all required fields configured</span>');
|
statusEl.innerHTML = '<span style="color:#10b981">✓ 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';
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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">×</button>
|
<button class="btn-remove" title="Remove">×</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">→</span>
|
<span class="arrow" style="font-size:12px">→</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">×</button>
|
<button class="btn-remove" title="Remove">×</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">×</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');
|
||||||
|
|||||||
Reference in New Issue
Block a user