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

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

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

https://claude.ai/code/session_01KF4i7Ra7zCEDskxDBaNtcT
This commit is contained in:
Claude
2026-03-29 18:45:10 +00:00
parent 73bc08dad2
commit 2719e1be44
17 changed files with 302 additions and 1902 deletions
+15 -93
View File
@@ -1,14 +1,14 @@
/**
* Silent Send - Auto-Detect
*
* Scans text for potential PII that the user hasn't configured.
* This catches things the identity and auto-redact scanner can't —
* Scans text for potential PPI that the user hasn't configured.
* This catches things the identity and secret scanner can't —
* because the user forgot or didn't know to configure them.
*
* Returns warnings (not auto-redactions) so the user can decide.
*/
const PII_PATTERNS = [
const PPI_PATTERNS = [
// --- Network ---
{
name: 'Private IP Address',
@@ -21,7 +21,7 @@ const PII_PATTERNS = [
regex: /\b(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\b/g,
category: 'network',
hint: 'IP address — could identify your network',
// Exclude common non-PII IPs
// Exclude common non-PPI IPs
exclude: /^(?:127\.0\.0\.1|0\.0\.0\.0|255\.255\.255\.\d+|8\.8\.[84]\.[84]|1\.1\.1\.1|1\.0\.0\.1)$/,
},
{
@@ -126,12 +126,12 @@ const PII_PATTERNS = [
},
];
// Context words that make ambiguous patterns more likely to be PII
// Context words that make ambiguous patterns more likely to be PPI
const CONTEXT_WORDS = /\b(?:born|birthday|dob|birth|passport|license|driver|ssn|social\s*security|address|home|live|lives|reside|zip|postal)\b/i;
const AutoDetect = {
/**
* Scan text for potential unconfigured PII.
* Scan text for potential unconfigured PPI.
* Pass in identity so we can skip values the user already configured.
*
* Returns array of { name, value, hint, category, index }
@@ -167,7 +167,7 @@ const AutoDetect = {
}
}
for (const pattern of PII_PATTERNS) {
for (const pattern of PPI_PATTERNS) {
// Skip context-dependent patterns if no context words present
if (pattern.contextRequired && !hasContext) continue;
@@ -221,15 +221,16 @@ const AutoDetect = {
*/
_detectProperNouns(text, configured) {
const findings = [];
// Only match TWO OR MORE consecutive capitalized words
// Single capitalized words cause too many false positives (sentence starts)
const re = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})+)\b/g;
const re = /(?:^|[.!?\n]\s*)?([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*)/g;
let m;
while ((m = re.exec(text)) !== null) {
const fullMatch = m[1];
if (!fullMatch) continue;
const before = text.slice(Math.max(0, m.index - 2), m.index);
const isSentenceStart = m.index === 0 || /[.!?\n]\s*$/.test(before);
const words = fullMatch.split(/\s+/);
const properWords = words.filter(w =>
w.length >= 3 &&
@@ -237,14 +238,15 @@ const AutoDetect = {
!configured.has(w.toLowerCase())
);
if (properWords.length < 2) continue; // need at least 2 proper words
if (properWords.length === 0) continue;
if (isSentenceStart && properWords.length === 1 && words.length === 1) continue;
const value = properWords.join(' ');
if (value.length >= 5 && !configured.has(value.toLowerCase())) {
if (value.length >= 3 && !configured.has(value.toLowerCase())) {
findings.push({
name: 'Possible Name/Org',
value,
hint: 'Capitalized phrase — could be a name, company, or project',
hint: 'Capitalized word — could be a name, company, or project',
category: 'name',
});
}
@@ -260,8 +262,6 @@ const AutoDetect = {
};
// Common English words to exclude from proper noun detection
// Comprehensive list including verbs, nouns, adjectives that appear
// in titles, headings, UI buttons, and instructions
const COMMON_WORDS = new Set([
'the', 'and', 'but', 'for', 'not', 'you', 'all', 'can', 'had', 'her',
'was', 'one', 'our', 'out', 'are', 'has', 'his', 'how', 'its', 'may',
@@ -281,84 +281,6 @@ const COMMON_WORDS = new Set([
'number', 'other', 'point', 'right', 'small', 'state', 'thing',
'think', 'those', 'three', 'through', 'under', 'until', 'water',
'world', 'write', 'might', 'should', 'because', 'although',
// Common verbs (titles, headings, buttons, instructions)
'generate', 'design', 'manage', 'process', 'handle', 'check', 'verify',
'submit', 'apply', 'accept', 'reject', 'approve', 'deny', 'confirm',
'cancel', 'delete', 'remove', 'edit', 'modify', 'view', 'display',
'search', 'filter', 'sort', 'select', 'choose', 'pick', 'enter',
'upload', 'download', 'install', 'enable', 'disable', 'activate',
'connect', 'disconnect', 'sync', 'refresh', 'reload', 'reset',
'save', 'load', 'store', 'restore', 'backup', 'copy', 'paste',
'lock', 'unlock', 'encrypt', 'decrypt', 'sign', 'register', 'login',
'logout', 'subscribe', 'share', 'publish', 'deploy', 'launch',
'merge', 'split', 'join', 'link', 'attach', 'insert', 'append',
'format', 'parse', 'convert', 'transform', 'translate', 'compile',
'execute', 'render', 'animate', 'validate', 'sanitize', 'escape',
'create', 'build', 'start', 'stop', 'open', 'close', 'run', 'send',
// Common nouns (titles, headings, labels)
'account', 'action', 'address', 'alert', 'analysis', 'application',
'area', 'article', 'asset', 'background', 'badge', 'banner', 'board',
'body', 'border', 'bottom', 'box', 'browser', 'buffer', 'button',
'cache', 'calendar', 'card', 'category', 'center', 'channel', 'chart',
'chat', 'child', 'choice', 'client', 'cloud', 'code', 'collection',
'color', 'column', 'command', 'comment', 'community', 'company',
'component', 'config', 'configuration', 'connection', 'console',
'contact', 'container', 'content', 'context', 'control', 'count',
'country', 'custom', 'dashboard', 'data', 'database', 'date', 'day',
'default', 'description', 'design', 'desktop', 'detail', 'device',
'dialog', 'directory', 'document', 'domain', 'draft', 'driver',
'edge', 'editor', 'element', 'email', 'engine', 'entry', 'environment',
'error', 'event', 'example', 'extension', 'feature', 'feedback',
'field', 'file', 'filter', 'folder', 'font', 'footer', 'form',
'frame', 'function', 'gallery', 'general', 'global', 'grid',
'guide', 'handler', 'header', 'health', 'help', 'history', 'home',
'host', 'icon', 'image', 'index', 'info', 'input', 'instance',
'interface', 'issue', 'item', 'job', 'key', 'label', 'language',
'layout', 'level', 'library', 'light', 'limit', 'line', 'link',
'list', 'local', 'location', 'log', 'logo', 'main', 'manager',
'manual', 'map', 'media', 'member', 'memory', 'menu', 'message',
'method', 'mobile', 'modal', 'mode', 'model', 'module', 'monitor',
'navigation', 'network', 'node', 'note', 'notification', 'object',
'option', 'order', 'origin', 'output', 'overlay', 'overview', 'owner',
'package', 'page', 'panel', 'parent', 'parser', 'password', 'path',
'pattern', 'permission', 'photo', 'pipeline', 'placeholder', 'plan',
'platform', 'player', 'plugin', 'point', 'policy', 'pool', 'popup',
'port', 'position', 'post', 'power', 'preview', 'primary', 'print',
'priority', 'process', 'product', 'profile', 'program', 'progress',
'project', 'prompt', 'property', 'protocol', 'provider', 'proxy',
'public', 'query', 'queue', 'quick', 'range', 'rate', 'reader',
'record', 'region', 'release', 'remote', 'report', 'request',
'resource', 'response', 'result', 'review', 'role', 'root', 'route',
'row', 'rule', 'runtime', 'sample', 'scanner', 'schema', 'scope',
'screen', 'script', 'search', 'section', 'security', 'select',
'sender', 'server', 'service', 'session', 'setting', 'settings',
'setup', 'share', 'shell', 'shortcut', 'sidebar', 'signal', 'simple',
'single', 'site', 'size', 'slider', 'snapshot', 'socket', 'solution',
'source', 'space', 'stage', 'standard', 'status', 'step', 'storage',
'stream', 'string', 'style', 'subject', 'success', 'summary',
'support', 'switch', 'symbol', 'syntax', 'system', 'table', 'target',
'task', 'team', 'template', 'terminal', 'test', 'text', 'theme',
'thread', 'title', 'token', 'tool', 'toolbar', 'tooltip', 'total',
'track', 'traffic', 'tree', 'trigger', 'type', 'unit', 'update',
'upload', 'user', 'utility', 'value', 'variable', 'version', 'video',
'view', 'virtual', 'warning', 'watch', 'web', 'widget', 'width',
'window', 'wizard', 'word', 'worker', 'workspace', 'wrapper', 'zone',
// Common adjectives
'active', 'advanced', 'available', 'basic', 'clean', 'clear', 'complete',
'connected', 'correct', 'critical', 'current', 'dark', 'deep',
'different', 'direct', 'double', 'dynamic', 'easy', 'empty', 'entire',
'exact', 'extra', 'fast', 'final', 'fixed', 'flat', 'free', 'fresh',
'full', 'generic', 'given', 'hidden', 'initial', 'inner', 'internal',
'invalid', 'latest', 'live', 'major', 'maximum', 'minimum', 'minor',
'mixed', 'modern', 'multiple', 'native', 'natural', 'nested', 'normal',
'online', 'optional', 'outer', 'overall', 'partial', 'pending', 'plain',
'popular', 'possible', 'previous', 'private', 'proper', 'protected',
'random', 'raw', 'ready', 'real', 'recent', 'related', 'relative',
'required', 'responsive', 'safe', 'secure', 'selected', 'sensitive',
'separate', 'shared', 'silent', 'similar', 'smart', 'smooth', 'solid',
'special', 'specific', 'stable', 'static', 'strict', 'strong',
'supported', 'unique', 'universal', 'unknown', 'upper', 'valid',
'various', 'visible', 'visual', 'whole', 'wide',
// Programming / tech terms
'string', 'number', 'boolean', 'object', 'array', 'function', 'class',
'type', 'error', 'null', 'undefined', 'true', 'false', 'return',
+5 -5
View File
@@ -12,7 +12,7 @@
* - Policy updates are applied automatically
*
* Privacy: the org admin can check compliance (are required fields
* configured?) but CANNOT see individual PII values.
* configured?) but CANNOT see individual PPI values.
*/
import api from './browser-polyfill.js';
@@ -198,11 +198,11 @@ const OrgPolicy = {
},
/**
* Get org-required auto-redact patterns.
* Get org-required secret scanner patterns.
*
* @returns {Array} additional patterns to add to auto-redact
* @returns {Array} additional patterns to add to the secret scanner
*/
async getOrgRedactPatterns() {
async getOrgSecretPatterns() {
const policy = await this.getPolicy();
if (!policy?.requiredSecretPatterns?.length) return [];
@@ -220,7 +220,7 @@ const OrgPolicy = {
/**
* Check if the user's configuration meets org policy requirements.
* Returns compliance status WITHOUT revealing actual PII values.
* Returns compliance status WITHOUT revealing actual PPI values.
*
* @returns {{ compliant: boolean, missing: string[], configured: string[] }}
*/
@@ -1,13 +1,11 @@
/**
* Silent Send - Auto Redact
* Silent Send - Secret Scanner
*
* Detects common secret/credential patterns in text and either
* warns or auto-redacts them. This catches things the identity-based
* smart patterns can't: API keys, tokens, passwords, SSNs, credit
* cards, private keys, connection strings, etc.
*
* Supports user-defined custom patterns for proprietary token formats.
*
* Each pattern has:
* - name: human-readable label
* - regex: detection pattern
@@ -15,7 +13,7 @@
* - severity: 'critical' (always redact) or 'warning' (flag but allow)
*/
const REDACT_PATTERNS = [
const SECRET_PATTERNS = [
// --- API Keys ---
{
name: 'OpenAI API Key',
@@ -163,39 +161,14 @@ const REDACT_PATTERNS = [
},
];
const AutoRedact = {
/**
* Build the full pattern list (built-in + custom).
* Custom patterns come from settings.customRedactPatterns.
*/
_buildPatterns(customPatterns) {
const all = [...REDACT_PATTERNS];
if (Array.isArray(customPatterns)) {
for (const cp of customPatterns) {
if (!cp.enabled || !cp.pattern) continue;
try {
all.push({
name: cp.name || 'Custom Pattern',
regex: new RegExp(cp.pattern, 'g'),
redact: cp.redact || '[REDACTED-CUSTOM]',
severity: 'critical',
});
} catch { /* invalid regex — skip */ }
}
}
return all;
},
const SecretScanner = {
/**
* Scan text for secrets. Returns list of findings.
* @param {string} text
* @param {Array} [customPatterns] from settings.customRedactPatterns
*/
scan(text, customPatterns) {
scan(text) {
const findings = [];
const patterns = this._buildPatterns(customPatterns);
for (const pattern of patterns) {
for (const pattern of SECRET_PATTERNS) {
// Reset regex lastIndex
pattern.regex.lastIndex = 0;
let match;
@@ -231,11 +204,9 @@ const AutoRedact = {
/**
* Redact all critical secrets in text. Warnings are not auto-redacted.
* Returns { text, redactions[] }
* @param {string} text
* @param {Array} [customPatterns] from settings.customRedactPatterns
*/
redact(text, customPatterns) {
const findings = this.scan(text, customPatterns);
redact(text) {
const findings = this.scan(text);
const redactions = [];
let result = text;
@@ -249,7 +220,7 @@ const AutoRedact = {
redactions.push({
original: f.value,
replaced: f.redactTo,
category: 'redact',
category: 'secret',
pattern: f.name,
});
}
@@ -266,7 +237,7 @@ const AutoRedact = {
};
if (typeof globalThis !== 'undefined') {
globalThis.AutoRedact = AutoRedact;
globalThis.SecretScanner = SecretScanner;
}
export default AutoRedact;
export default SecretScanner;
+3 -4
View File
@@ -23,7 +23,7 @@ const KEYS = {
SETTINGS: 'ss_settings',
};
// Keys that contain sensitive PII and should be encrypted at rest
// Keys that contain sensitive PPI and should be encrypted at rest
// All user data keys are encrypted at rest — settings included since
// custom domains and configuration can reveal what services the user
// accesses. Only ss_sync_encryption (salt, verification blob) and
@@ -34,13 +34,12 @@ const DEFAULT_SETTINGS = {
enabled: true,
showHighlights: false,
revealMode: false,
autoRedact: true,
secretScanning: true,
autoDetect: true,
autoRedactDetected: true,
autoAddDetected: true,
maxLogEntries: 100,
maxLogEntries: 200,
customDomains: [],
customRedactPatterns: [],
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
browserSync: false,
};
+11 -24
View File
@@ -12,9 +12,8 @@
* 5. Custom HTTP endpoint — any URL supporting GET + PUT (WebDAV,
* self-hosted server, cloud function, etc.).
*
* Encryption: all sync channels REQUIRE encryption with a password
* (AES-256-GCM) and/or TOTP verification. Syncing without encryption
* is not permitted — users must set up encryption before enabling sync.
* Encryption: all sync channels can optionally encrypt data with a
* password (AES-256-GCM) and/or require TOTP verification.
* Authentication is cached with a configurable TTL so the user only
* needs to authenticate when the cache expires and new data exists.
*
@@ -371,9 +370,6 @@ const SilentSendSync = {
const StorageModule = (await import('./storage.js')).default;
await StorageModule.decryptAllData();
// Disable all sync channels since encryption is mandatory for sync
await StorageModule.saveSettings({ browserSync: false });
await api.storage.local.remove('ss_sync_encryption');
await SilentSendCrypto.clearCachedKey();
await SilentSendCrypto.clearWebAuthnCredential();
@@ -419,7 +415,7 @@ const SilentSendSync = {
*/
async _encryptForSync(data) {
const config = await this._getSyncEncryption();
if (!config?.enabled) return { data: null, encrypted: false, needsEncryption: true };
if (!config?.enabled) return { data, encrypted: false };
const keyInfo = await this._getEncryptionKey();
if (!keyInfo) {
@@ -583,15 +579,12 @@ const SilentSendSync = {
async exportSyncCode() {
const data = await this._getAllData();
// Encrypt (mandatory)
// Encrypt if enabled
const result = await this._encryptForSync(data);
if (result.needsEncryption) {
return { needsEncryption: true };
}
if (result.needsAuth) {
return { needsAuth: true };
}
const payload = result.data;
const payload = result.data || data;
const json = JSON.stringify(payload);
return btoa(unescape(encodeURIComponent(json)));
@@ -647,10 +640,10 @@ const SilentSendSync = {
try {
const data = await this._getAllData();
// Encrypt (mandatory)
// Encrypt if enabled
const result = await this._encryptForSync(data);
if (result.needsEncryption || result.needsAuth) return; // skip — encryption required
const payload = result.data;
if (result.needsAuth) return; // silently skip — will sync on next auth
const payload = result.data || data;
const json = JSON.stringify(payload);
@@ -716,15 +709,12 @@ const SilentSendSync = {
try {
const data = await this._getAllData();
// Encrypt (mandatory)
// Encrypt if enabled
const encResult = await this._encryptForSync(data);
if (encResult.needsEncryption) {
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
}
if (encResult.needsAuth) {
return { success: false, needsAuth: true, reason: 'Authentication required.' };
}
const payload = encResult.data;
const payload = encResult.data || data;
const content = JSON.stringify(payload, null, 2);
const stored = await api.storage.local.get('ss_gist_id');
@@ -820,13 +810,10 @@ const SilentSendSync = {
const data = await this._getAllData();
const encResult = await this._encryptForSync(data);
if (encResult.needsEncryption) {
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
}
if (encResult.needsAuth) {
return { success: false, needsAuth: true, reason: 'Authentication required.' };
}
const payload = encResult.data;
const payload = encResult.data || data;
const resp = await fetch(url, {
method,