feat: add smart pattern detection for emails, names, usernames, phones
Instead of requiring explicit mappings for every variation, users now configure their identity once (Identity tab) and Silent Send auto-catches: - Emails: any address @gmail, @yahoo, @outlook, etc. - Names: first/last, full name, reversed, possessives, case variants - Usernames: user@host, ~user, /home/user, C:\Users\user - Phones: all common formats ((555) 123-4567, 555.123.4567, etc.) Smart patterns run before explicit mappings, so explicit rules can override smart catches when needed. https://claude.ai/code/session_01Dvgwe7XMoSxnWXkih8p1Cw
This commit is contained in:
+222
-41
@@ -14,6 +14,7 @@
|
|||||||
// Load config from the injector script's data attribute
|
// Load config from the injector script's data attribute
|
||||||
// ============================================================
|
// ============================================================
|
||||||
let mappings = [];
|
let mappings = [];
|
||||||
|
let identity = {};
|
||||||
let settings = { enabled: true, revealMode: false, showHighlights: false };
|
let settings = { enabled: true, revealMode: false, showHighlights: false };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
if (configEl) {
|
if (configEl) {
|
||||||
const config = JSON.parse(configEl.getAttribute('data-ss-config'));
|
const config = JSON.parse(configEl.getAttribute('data-ss-config'));
|
||||||
mappings = config.mappings || [];
|
mappings = config.mappings || [];
|
||||||
|
identity = config.identity || {};
|
||||||
settings = { ...settings, ...(config.settings || {}) };
|
settings = { ...settings, ...(config.settings || {}) };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
if (event.source !== window) return;
|
if (event.source !== window) return;
|
||||||
if (event.data?.type === 'ss:config-updated') {
|
if (event.data?.type === 'ss:config-updated') {
|
||||||
if (event.data.mappings) mappings = event.data.mappings;
|
if (event.data.mappings) mappings = event.data.mappings;
|
||||||
|
if (event.data.identity) identity = event.data.identity;
|
||||||
if (event.data.settings) settings = { ...settings, ...event.data.settings };
|
if (event.data.settings) settings = { ...settings, ...event.data.settings };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -46,7 +49,7 @@
|
|||||||
|
|
||||||
for (const m of sorted) {
|
for (const m of sorted) {
|
||||||
if (!m.enabled || !m.real || !m.substitute) continue;
|
if (!m.enabled || !m.real || !m.substitute) continue;
|
||||||
const escaped = m.real.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escaped = esc(m.real);
|
||||||
const regex = new RegExp(escaped, m.caseSensitive ? 'g' : 'gi');
|
const regex = new RegExp(escaped, m.caseSensitive ? 'g' : 'gi');
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(result)) !== null) {
|
while ((match = regex.exec(result)) !== null) {
|
||||||
@@ -66,13 +69,191 @@
|
|||||||
const sorted = [...maps].sort((a, b) => b.substitute.length - a.substitute.length);
|
const sorted = [...maps].sort((a, b) => b.substitute.length - a.substitute.length);
|
||||||
for (const m of sorted) {
|
for (const m of sorted) {
|
||||||
if (!m.enabled || !m.real || !m.substitute) continue;
|
if (!m.enabled || !m.real || !m.substitute) continue;
|
||||||
const escaped = m.substitute.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escaped = esc(m.substitute);
|
||||||
const regex = new RegExp(escaped, m.caseSensitive ? 'g' : 'gi');
|
const regex = new RegExp(escaped, m.caseSensitive ? 'g' : 'gi');
|
||||||
result = result.replace(regex, m.real);
|
result = result.replace(regex, m.real);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function esc(str) {
|
||||||
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Smart Pattern Engine (inline for page world)
|
||||||
|
// ============================================================
|
||||||
|
const COMMON_EMAIL_DOMAINS = new Set([
|
||||||
|
'gmail.com', 'googlemail.com', 'yahoo.com', 'yahoo.co.uk',
|
||||||
|
'hotmail.com', 'outlook.com', 'live.com', 'msn.com',
|
||||||
|
'icloud.com', 'me.com', 'mac.com',
|
||||||
|
'aol.com', 'proton.me', 'protonmail.com',
|
||||||
|
'mail.com', 'zoho.com', 'fastmail.com',
|
||||||
|
'yandex.com', 'gmx.com', 'gmx.net',
|
||||||
|
'comcast.net', 'verizon.net', 'att.net', 'cox.net',
|
||||||
|
'sbcglobal.net', 'charter.net', 'bellsouth.net',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function smartSubstitute(text, id) {
|
||||||
|
if (!id || !id.enabled) return { text, replacements: [] };
|
||||||
|
const replacements = [];
|
||||||
|
let result = text;
|
||||||
|
|
||||||
|
// Emails
|
||||||
|
if (id.enabled.emails !== false) {
|
||||||
|
const emailMap = new Map();
|
||||||
|
for (const e of (id.emails || [])) {
|
||||||
|
emailMap.set(e.real.toLowerCase(), e.substitute);
|
||||||
|
}
|
||||||
|
const myDomains = new Set((id.emailDomains || []).map(d => d.toLowerCase()));
|
||||||
|
const emailRegex = /\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/g;
|
||||||
|
const matches = [];
|
||||||
|
let m;
|
||||||
|
while ((m = emailRegex.exec(result)) !== null) {
|
||||||
|
matches.push({ index: m.index, value: m[0] });
|
||||||
|
}
|
||||||
|
for (let i = matches.length - 1; i >= 0; i--) {
|
||||||
|
const em = matches[i];
|
||||||
|
const lower = em.value.toLowerCase();
|
||||||
|
const domain = lower.split('@')[1];
|
||||||
|
let replacement = null;
|
||||||
|
if (emailMap.has(lower)) {
|
||||||
|
replacement = emailMap.get(lower);
|
||||||
|
} else if (COMMON_EMAIL_DOMAINS.has(domain) || myDomains.has(domain)) {
|
||||||
|
replacement = id.catchAllEmail || 'user@example.com';
|
||||||
|
}
|
||||||
|
if (replacement) {
|
||||||
|
replacements.push({ original: em.value, replaced: replacement, category: 'email', pattern: 'smart' });
|
||||||
|
result = result.slice(0, em.index) + replacement + result.slice(em.index + em.value.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phones
|
||||||
|
if (id.enabled.phones !== false) {
|
||||||
|
for (const p of (id.phones || [])) {
|
||||||
|
if (!p.real || !p.substitute) continue;
|
||||||
|
const digits = p.real.replace(/\D/g, '');
|
||||||
|
if (digits.length < 7) continue;
|
||||||
|
const d = digits.startsWith('1') && digits.length === 11 ? digits.slice(1) : digits;
|
||||||
|
if (d.length !== 10 && d.length !== 7) continue;
|
||||||
|
let pattern;
|
||||||
|
if (d.length === 10) {
|
||||||
|
const a = d.slice(0, 3), b = d.slice(3, 6), c = d.slice(6);
|
||||||
|
pattern = '(?:\\+?1[\\s.-]?)?(?:' + esc(a) + '|\\(' + esc(a) + '\\))[\\s.\\-]?' + esc(b) + '[\\s.\\-]?' + esc(c);
|
||||||
|
} else {
|
||||||
|
pattern = esc(d.slice(0, 3)) + '[\\s.\\-]?' + esc(d.slice(3));
|
||||||
|
}
|
||||||
|
const phoneRegex = new RegExp(pattern, 'g');
|
||||||
|
result = result.replace(phoneRegex, (matched) => {
|
||||||
|
replacements.push({ original: matched, replaced: p.substitute, category: 'phone', pattern: 'smart' });
|
||||||
|
return p.substitute;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names (full names first, then individual)
|
||||||
|
if (id.enabled.names !== false) {
|
||||||
|
const names = id.names || [];
|
||||||
|
const firsts = names.filter(n => n.type === 'first');
|
||||||
|
const lasts = names.filter(n => n.type === 'last');
|
||||||
|
|
||||||
|
for (const first of firsts) {
|
||||||
|
for (const last of lasts) {
|
||||||
|
// "First Last"
|
||||||
|
result = result.replace(new RegExp(esc(first.real) + '\\s+' + esc(last.real), 'gi'), (matched) => {
|
||||||
|
const sub = `${first.substitute} ${last.substitute}`;
|
||||||
|
replacements.push({ original: matched, replaced: sub, category: 'name', pattern: 'smart' });
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
// "Last, First"
|
||||||
|
result = result.replace(new RegExp(esc(last.real) + ',\\s*' + esc(first.real), 'gi'), (matched) => {
|
||||||
|
const sub = `${last.substitute}, ${first.substitute}`;
|
||||||
|
replacements.push({ original: matched, replaced: sub, category: 'name', pattern: 'smart' });
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
if (!name.real || !name.substitute) continue;
|
||||||
|
result = result.replace(new RegExp('\\b' + esc(name.real) + "(?:'s)?\\b", 'gi'), (matched) => {
|
||||||
|
const isPossessive = matched.endsWith("'s");
|
||||||
|
const sub = isPossessive ? name.substitute + "'s" : name.substitute;
|
||||||
|
replacements.push({ original: matched, replaced: sub, category: 'name', pattern: 'smart' });
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usernames + paths
|
||||||
|
if (id.enabled.usernames !== false) {
|
||||||
|
for (const u of (id.usernames || [])) {
|
||||||
|
if (!u.real || !u.substitute) continue;
|
||||||
|
|
||||||
|
// user@hostname
|
||||||
|
result = result.replace(new RegExp(esc(u.real) + '@[a-zA-Z0-9._\\-]+', 'g'), (matched) => {
|
||||||
|
const host = matched.slice(u.real.length + 1);
|
||||||
|
const sub = u.substitute + '@' + host;
|
||||||
|
replacements.push({ original: matched, replaced: sub, category: 'username', pattern: 'smart' });
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ~username
|
||||||
|
result = result.replace(new RegExp('~' + esc(u.real) + '\\b', 'g'), (matched) => {
|
||||||
|
const sub = '~' + u.substitute;
|
||||||
|
replacements.push({ original: matched, replaced: sub, category: 'username', pattern: 'smart' });
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
|
||||||
|
// /home/username, /Users/username
|
||||||
|
result = result.replace(new RegExp('(/(?:home|Users)/)' + esc(u.real) + '(?=/|\\s|$|"|\')', 'g'), (matched, prefix) => {
|
||||||
|
const sub = prefix + u.substitute;
|
||||||
|
replacements.push({ original: matched, replaced: sub, category: 'path', pattern: 'smart' });
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
|
||||||
|
// C:\Users\username
|
||||||
|
result = result.replace(new RegExp('([A-Z]:\\\\Users\\\\)' + esc(u.real) + '(?=\\\\|\\s|$|"|\')', 'gi'), (matched, prefix) => {
|
||||||
|
const sub = prefix + u.substitute;
|
||||||
|
replacements.push({ original: matched, replaced: sub, category: 'path', pattern: 'smart' });
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
|
||||||
|
// plain username (3+ chars to avoid false positives)
|
||||||
|
if (u.real.length >= 3) {
|
||||||
|
result = result.replace(new RegExp('\\b' + esc(u.real) + '\\b', 'g'), (matched) => {
|
||||||
|
replacements.push({ original: matched, replaced: u.substitute, category: 'username', pattern: 'smart' });
|
||||||
|
return u.substitute;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: result, replacements };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Combined substitution: smart patterns first, then explicit
|
||||||
|
// ============================================================
|
||||||
|
function substituteAll(text) {
|
||||||
|
const allReplacements = [];
|
||||||
|
|
||||||
|
// Smart patterns (broad catches)
|
||||||
|
const smart = smartSubstitute(text, identity);
|
||||||
|
allReplacements.push(...smart.replacements);
|
||||||
|
|
||||||
|
// Explicit mappings (specific overrides)
|
||||||
|
const explicit = substitute(smart.text, mappings);
|
||||||
|
allReplacements.push(...explicit.replacements);
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: explicit.text,
|
||||||
|
replacements: allReplacements,
|
||||||
|
modified: allReplacements.length > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Notify content script of substitutions (for badge + logging)
|
// Notify content script of substitutions (for badge + logging)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -91,14 +272,19 @@
|
|||||||
let modified = false;
|
let modified = false;
|
||||||
const allReplacements = [];
|
const allReplacements = [];
|
||||||
|
|
||||||
// Shape 1: { prompt: "..." }
|
function processText(text) {
|
||||||
if (typeof body.prompt === 'string') {
|
const r = substituteAll(text);
|
||||||
const r = substitute(body.prompt, mappings);
|
if (r.modified) {
|
||||||
if (r.replacements.length > 0) {
|
|
||||||
body.prompt = r.text;
|
|
||||||
allReplacements.push(...r.replacements);
|
allReplacements.push(...r.replacements);
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape 1: { prompt: "..." }
|
||||||
|
if (typeof body.prompt === 'string') {
|
||||||
|
const r = processText(body.prompt);
|
||||||
|
if (r.modified) body.prompt = r.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shape 2: { content: [{ type: "text", text: "..." }] }
|
// Shape 2: { content: [{ type: "text", text: "..." }] }
|
||||||
@@ -106,12 +292,8 @@
|
|||||||
for (let i = 0; i < body.content.length; i++) {
|
for (let i = 0; i < body.content.length; i++) {
|
||||||
const item = body.content[i];
|
const item = body.content[i];
|
||||||
if (item.type === 'text' && typeof item.text === 'string') {
|
if (item.type === 'text' && typeof item.text === 'string') {
|
||||||
const r = substitute(item.text, mappings);
|
const r = processText(item.text);
|
||||||
if (r.replacements.length > 0) {
|
if (r.modified) body.content[i] = { ...item, text: r.text };
|
||||||
body.content[i] = { ...item, text: r.text };
|
|
||||||
allReplacements.push(...r.replacements);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,23 +304,15 @@
|
|||||||
if (msg.role !== 'user' && msg.role !== 'human') continue;
|
if (msg.role !== 'user' && msg.role !== 'human') continue;
|
||||||
|
|
||||||
if (typeof msg.content === 'string') {
|
if (typeof msg.content === 'string') {
|
||||||
const r = substitute(msg.content, mappings);
|
const r = processText(msg.content);
|
||||||
if (r.replacements.length > 0) {
|
if (r.modified) msg.content = r.text;
|
||||||
msg.content = r.text;
|
|
||||||
allReplacements.push(...r.replacements);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(msg.content)) {
|
if (Array.isArray(msg.content)) {
|
||||||
for (let j = 0; j < msg.content.length; j++) {
|
for (let j = 0; j < msg.content.length; j++) {
|
||||||
if (msg.content[j].type === 'text') {
|
if (msg.content[j].type === 'text') {
|
||||||
const r = substitute(msg.content[j].text, mappings);
|
const r = processText(msg.content[j].text);
|
||||||
if (r.replacements.length > 0) {
|
if (r.modified) msg.content[j] = { ...msg.content[j], text: r.text };
|
||||||
msg.content[j] = { ...msg.content[j], text: r.text };
|
|
||||||
allReplacements.push(...r.replacements);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,13 +322,24 @@
|
|||||||
return { modified, replacements: allReplacements };
|
return { modified, replacements: allReplacements };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Check if we have anything to substitute
|
||||||
|
// ============================================================
|
||||||
|
function hasSubstitutions() {
|
||||||
|
return mappings.length > 0 ||
|
||||||
|
(identity.emails || []).length > 0 ||
|
||||||
|
(identity.names || []).length > 0 ||
|
||||||
|
(identity.usernames || []).length > 0 ||
|
||||||
|
(identity.phones || []).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Fetch Interception
|
// Fetch Interception
|
||||||
// ============================================================
|
// ============================================================
|
||||||
const originalFetch = window.fetch;
|
const originalFetch = window.fetch;
|
||||||
|
|
||||||
window.fetch = async function (url, options) {
|
window.fetch = async function (url, options) {
|
||||||
if (!settings.enabled || mappings.length === 0) {
|
if (!settings.enabled || !hasSubstitutions()) {
|
||||||
return originalFetch.call(this, url, options);
|
return originalFetch.call(this, url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +383,7 @@
|
|||||||
|
|
||||||
XMLHttpRequest.prototype.send = function (body) {
|
XMLHttpRequest.prototype.send = function (body) {
|
||||||
if (
|
if (
|
||||||
settings.enabled && mappings.length > 0 &&
|
settings.enabled && hasSubstitutions() &&
|
||||||
typeof body === 'string' && this._ssUrl &&
|
typeof body === 'string' && this._ssUrl &&
|
||||||
(this._ssUrl.includes('/chat_conversations/') ||
|
(this._ssUrl.includes('/chat_conversations/') ||
|
||||||
this._ssUrl.includes('/completion') ||
|
this._ssUrl.includes('/completion') ||
|
||||||
@@ -221,13 +406,12 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
function observeResponses() {
|
function observeResponses() {
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
if (!settings.revealMode || mappings.length === 0) return;
|
if (!settings.revealMode || !hasSubstitutions()) return;
|
||||||
|
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
for (const node of mutation.addedNodes) {
|
for (const node of mutation.addedNodes) {
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||||
|
|
||||||
// Claude response selectors
|
|
||||||
const responseEls = node.querySelectorAll
|
const responseEls = node.querySelectorAll
|
||||||
? node.querySelectorAll('[data-is-streaming], .font-claude-message, .prose, [class*="Message"]')
|
? node.querySelectorAll('[data-is-streaming], .font-claude-message, .prose, [class*="Message"]')
|
||||||
: [];
|
: [];
|
||||||
@@ -272,7 +456,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial + periodic scan
|
|
||||||
walk(document);
|
walk(document);
|
||||||
setInterval(() => walk(document), 3000);
|
setInterval(() => walk(document), 3000);
|
||||||
}
|
}
|
||||||
@@ -281,19 +464,12 @@
|
|||||||
// Input Highlighting
|
// Input Highlighting
|
||||||
// ============================================================
|
// ============================================================
|
||||||
document.addEventListener('input', (e) => {
|
document.addEventListener('input', (e) => {
|
||||||
if (!settings.showHighlights || mappings.length === 0) return;
|
if (!settings.showHighlights || !hasSubstitutions()) return;
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
|
if (target.matches?.('[contenteditable], textarea, input[type="text"]')) {
|
||||||
const text = target.textContent || target.value || '';
|
const text = target.textContent || target.value || '';
|
||||||
let hasMatches = false;
|
const r = substituteAll(text);
|
||||||
for (const m of mappings) {
|
target.classList.toggle('ss-has-sensitive', r.modified);
|
||||||
if (!m.enabled || !m.real) continue;
|
|
||||||
if (text.toLowerCase().includes(m.real.toLowerCase())) {
|
|
||||||
hasMatches = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target.classList.toggle('ss-has-sensitive', hasMatches);
|
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
@@ -307,7 +483,12 @@
|
|||||||
}
|
}
|
||||||
traverseShadowRoots();
|
traverseShadowRoots();
|
||||||
|
|
||||||
|
const smartCount = (identity.emails || []).length +
|
||||||
|
(identity.names || []).length +
|
||||||
|
(identity.usernames || []).length +
|
||||||
|
(identity.phones || []).length;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[Silent Send] Active on ${location.hostname} with ${mappings.length} mapping(s)`
|
`[Silent Send] Active on ${location.hostname} — ${mappings.length} explicit mapping(s), ${smartCount} smart pattern(s)`
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ const api =
|
|||||||
|
|
||||||
// Load mappings and settings, then inject into page
|
// Load mappings and settings, then inject into page
|
||||||
async function init() {
|
async function init() {
|
||||||
const result = await api.storage.local.get(['ss_mappings', 'ss_settings']);
|
const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']);
|
||||||
const mappings = result.ss_mappings || [];
|
const mappings = result.ss_mappings || [];
|
||||||
|
const identity = result.ss_identity || {};
|
||||||
const settings = result.ss_settings || { enabled: true };
|
const settings = result.ss_settings || { enabled: true };
|
||||||
|
|
||||||
// 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, settings }));
|
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();
|
||||||
@@ -45,10 +46,11 @@ async function init() {
|
|||||||
|
|
||||||
// Forward storage changes to the page script
|
// Forward storage changes to the page script
|
||||||
api.storage.onChanged.addListener((changes) => {
|
api.storage.onChanged.addListener((changes) => {
|
||||||
if (changes.ss_mappings || changes.ss_settings) {
|
if (changes.ss_mappings || changes.ss_identity || changes.ss_settings) {
|
||||||
window.postMessage({
|
window.postMessage({
|
||||||
type: 'ss:config-updated',
|
type: 'ss:config-updated',
|
||||||
mappings: changes.ss_mappings?.newValue,
|
mappings: changes.ss_mappings?.newValue,
|
||||||
|
identity: changes.ss_identity?.newValue,
|
||||||
settings: changes.ss_settings?.newValue,
|
settings: changes.ss_settings?.newValue,
|
||||||
}, '*');
|
}, '*');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,392 @@
|
|||||||
|
/**
|
||||||
|
* Silent Send - Smart Pattern Detector
|
||||||
|
*
|
||||||
|
* Automatically detects and substitutes personal data patterns
|
||||||
|
* without requiring explicit mappings for every variation.
|
||||||
|
*
|
||||||
|
* Supported patterns:
|
||||||
|
* - Emails: anything@recognized-domain → substitute-email
|
||||||
|
* - Names: First, Last, First Last, LAST (case variants)
|
||||||
|
* - User@Host: username patterns from system/shell contexts
|
||||||
|
* - Phones: common formats (xxx) xxx-xxxx, xxx-xxx-xxxx, etc.
|
||||||
|
* - Paths: /home/username, /Users/username, C:\Users\username
|
||||||
|
*/
|
||||||
|
|
||||||
|
const COMMON_EMAIL_DOMAINS = new Set([
|
||||||
|
'gmail.com', 'googlemail.com', 'yahoo.com', 'yahoo.co.uk',
|
||||||
|
'hotmail.com', 'outlook.com', 'live.com', 'msn.com',
|
||||||
|
'icloud.com', 'me.com', 'mac.com',
|
||||||
|
'aol.com', 'proton.me', 'protonmail.com',
|
||||||
|
'mail.com', 'zoho.com', 'fastmail.com',
|
||||||
|
'yandex.com', 'gmx.com', 'gmx.net',
|
||||||
|
'comcast.net', 'verizon.net', 'att.net', 'cox.net',
|
||||||
|
'sbcglobal.net', 'charter.net', 'bellsouth.net',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const SmartPatterns = {
|
||||||
|
/**
|
||||||
|
* Process text using smart patterns + identity config.
|
||||||
|
* Returns { text, replacements[] } same shape as SubstitutionEngine.
|
||||||
|
*
|
||||||
|
* @param {string} text - Input text
|
||||||
|
* @param {object} identity - User's identity config:
|
||||||
|
* {
|
||||||
|
* emails: [{ real: "john@gmail.com", substitute: "alex@example.com" }],
|
||||||
|
* names: [{ real: "John", substitute: "Alex", type: "first" },
|
||||||
|
* { real: "Smith", substitute: "Demo", type: "last" }],
|
||||||
|
* usernames: [{ real: "jsmith", substitute: "ademo" }],
|
||||||
|
* phones: [{ real: "555-123-4567", substitute: "555-000-0000" }],
|
||||||
|
* catchAllEmail: "anon@example.com", // fallback for unknown emails with your domain
|
||||||
|
* emailDomains: ["mycompany.com"], // additional domains to catch
|
||||||
|
* enabled: { emails: true, names: true, usernames: true, phones: true, paths: true }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
substitute(text, identity) {
|
||||||
|
if (!identity) return { text, replacements: [] };
|
||||||
|
|
||||||
|
const replacements = [];
|
||||||
|
let result = text;
|
||||||
|
|
||||||
|
// Order matters: do emails first (most specific), then names, then usernames, then paths
|
||||||
|
if (identity.enabled?.emails !== false) {
|
||||||
|
const r = this._substituteEmails(result, identity);
|
||||||
|
result = r.text;
|
||||||
|
replacements.push(...r.replacements);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity.enabled?.phones !== false) {
|
||||||
|
const r = this._substitutePhones(result, identity);
|
||||||
|
result = r.text;
|
||||||
|
replacements.push(...r.replacements);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity.enabled?.names !== false) {
|
||||||
|
const r = this._substituteNames(result, identity);
|
||||||
|
result = r.text;
|
||||||
|
replacements.push(...r.replacements);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity.enabled?.usernames !== false) {
|
||||||
|
const r = this._substituteUsernames(result, identity);
|
||||||
|
result = r.text;
|
||||||
|
replacements.push(...r.replacements);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity.enabled?.paths !== false) {
|
||||||
|
const r = this._substitutePaths(result, identity);
|
||||||
|
result = r.text;
|
||||||
|
replacements.push(...r.replacements);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: result, replacements };
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----- Emails -----
|
||||||
|
// Catches: exact matches, AND any something@known-domain
|
||||||
|
_substituteEmails(text, identity) {
|
||||||
|
const replacements = [];
|
||||||
|
let result = text;
|
||||||
|
|
||||||
|
// Build set of known real emails for exact matching
|
||||||
|
const emailMap = new Map();
|
||||||
|
for (const e of (identity.emails || [])) {
|
||||||
|
emailMap.set(e.real.toLowerCase(), e.substitute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional domains to treat as "yours"
|
||||||
|
const myDomains = new Set(
|
||||||
|
(identity.emailDomains || []).map(d => d.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Match all email-like patterns
|
||||||
|
const emailRegex = /\b[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
// Collect all matches first, then replace from end to preserve indices
|
||||||
|
const matches = [];
|
||||||
|
while ((match = emailRegex.exec(result)) !== null) {
|
||||||
|
matches.push({ index: match.index, value: match[0] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process from end to start so indices stay valid
|
||||||
|
for (let i = matches.length - 1; i >= 0; i--) {
|
||||||
|
const m = matches[i];
|
||||||
|
const lower = m.value.toLowerCase();
|
||||||
|
const domain = lower.split('@')[1];
|
||||||
|
|
||||||
|
let replacement = null;
|
||||||
|
|
||||||
|
// Exact match?
|
||||||
|
if (emailMap.has(lower)) {
|
||||||
|
replacement = emailMap.get(lower);
|
||||||
|
}
|
||||||
|
// Known personal domain (gmail, etc.) or custom domain?
|
||||||
|
else if (
|
||||||
|
COMMON_EMAIL_DOMAINS.has(domain) ||
|
||||||
|
myDomains.has(domain)
|
||||||
|
) {
|
||||||
|
replacement = identity.catchAllEmail || 'user@example.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replacement) {
|
||||||
|
replacements.push({
|
||||||
|
original: m.value,
|
||||||
|
replaced: replacement,
|
||||||
|
category: 'email',
|
||||||
|
pattern: 'smart',
|
||||||
|
});
|
||||||
|
result =
|
||||||
|
result.slice(0, m.index) +
|
||||||
|
replacement +
|
||||||
|
result.slice(m.index + m.value.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: result, replacements };
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----- Names -----
|
||||||
|
// Catches: "John Smith", "Smith, John", "John", "Smith", "SMITH", "john"
|
||||||
|
// Also catches possessives: "John's", "Smith's"
|
||||||
|
_substituteNames(text, identity) {
|
||||||
|
const replacements = [];
|
||||||
|
let result = text;
|
||||||
|
const names = identity.names || [];
|
||||||
|
|
||||||
|
if (names.length === 0) return { text: result, replacements };
|
||||||
|
|
||||||
|
// First pass: full name combinations (first + last)
|
||||||
|
const firsts = names.filter(n => n.type === 'first');
|
||||||
|
const lasts = names.filter(n => n.type === 'last');
|
||||||
|
|
||||||
|
for (const first of firsts) {
|
||||||
|
for (const last of lasts) {
|
||||||
|
// "First Last"
|
||||||
|
const fullRegex = new RegExp(
|
||||||
|
esc(first.real) + "\\s+" + esc(last.real),
|
||||||
|
'gi'
|
||||||
|
);
|
||||||
|
result = result.replace(fullRegex, (matched) => {
|
||||||
|
replacements.push({
|
||||||
|
original: matched,
|
||||||
|
replaced: `${first.substitute} ${last.substitute}`,
|
||||||
|
category: 'name',
|
||||||
|
pattern: 'smart-fullname',
|
||||||
|
});
|
||||||
|
return `${first.substitute} ${last.substitute}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// "Last, First"
|
||||||
|
const reverseRegex = new RegExp(
|
||||||
|
esc(last.real) + ",\\s*" + esc(first.real),
|
||||||
|
'gi'
|
||||||
|
);
|
||||||
|
result = result.replace(reverseRegex, (matched) => {
|
||||||
|
replacements.push({
|
||||||
|
original: matched,
|
||||||
|
replaced: `${last.substitute}, ${first.substitute}`,
|
||||||
|
category: 'name',
|
||||||
|
pattern: 'smart-fullname-reverse',
|
||||||
|
});
|
||||||
|
return `${last.substitute}, ${first.substitute}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: individual names (with word boundaries)
|
||||||
|
for (const name of names) {
|
||||||
|
if (!name.real || !name.substitute) continue;
|
||||||
|
|
||||||
|
// Match the name with word boundaries, including possessives
|
||||||
|
const nameRegex = new RegExp(
|
||||||
|
'\\b' + esc(name.real) + "(?:'s)?\\b",
|
||||||
|
'gi'
|
||||||
|
);
|
||||||
|
|
||||||
|
result = result.replace(nameRegex, (matched) => {
|
||||||
|
const isPossessive = matched.endsWith("'s");
|
||||||
|
const sub = isPossessive
|
||||||
|
? name.substitute + "'s"
|
||||||
|
: name.substitute;
|
||||||
|
|
||||||
|
replacements.push({
|
||||||
|
original: matched,
|
||||||
|
replaced: sub,
|
||||||
|
category: 'name',
|
||||||
|
pattern: 'smart-name',
|
||||||
|
});
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: result, replacements };
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----- Usernames -----
|
||||||
|
// Catches: user@hostname, ~username, /home/username, mentions of username
|
||||||
|
// in shell/code contexts
|
||||||
|
_substituteUsernames(text, identity) {
|
||||||
|
const replacements = [];
|
||||||
|
let result = text;
|
||||||
|
const usernames = identity.usernames || [];
|
||||||
|
|
||||||
|
for (const u of usernames) {
|
||||||
|
if (!u.real || !u.substitute) continue;
|
||||||
|
|
||||||
|
// user@hostname patterns (SSH, terminal prompts)
|
||||||
|
const userHostRegex = new RegExp(
|
||||||
|
esc(u.real) + '@[a-zA-Z0-9._\\-]+',
|
||||||
|
'g'
|
||||||
|
);
|
||||||
|
result = result.replace(userHostRegex, (matched) => {
|
||||||
|
const host = matched.slice(u.real.length + 1);
|
||||||
|
const sub = u.substitute + '@' + host;
|
||||||
|
replacements.push({
|
||||||
|
original: matched,
|
||||||
|
replaced: sub,
|
||||||
|
category: 'username',
|
||||||
|
pattern: 'smart-userhost',
|
||||||
|
});
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ~username (shell shorthand)
|
||||||
|
const tildeRegex = new RegExp('~' + esc(u.real) + '\\b', 'g');
|
||||||
|
result = result.replace(tildeRegex, (matched) => {
|
||||||
|
const sub = '~' + u.substitute;
|
||||||
|
replacements.push({
|
||||||
|
original: matched,
|
||||||
|
replaced: sub,
|
||||||
|
category: 'username',
|
||||||
|
pattern: 'smart-tilde',
|
||||||
|
});
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Plain username with word boundaries (careful - short names can over-match)
|
||||||
|
// Only match if username is 3+ chars to avoid false positives
|
||||||
|
if (u.real.length >= 3) {
|
||||||
|
const plainRegex = new RegExp('\\b' + esc(u.real) + '\\b', 'g');
|
||||||
|
result = result.replace(plainRegex, (matched) => {
|
||||||
|
replacements.push({
|
||||||
|
original: matched,
|
||||||
|
replaced: u.substitute,
|
||||||
|
category: 'username',
|
||||||
|
pattern: 'smart-username',
|
||||||
|
});
|
||||||
|
return u.substitute;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: result, replacements };
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----- Phones -----
|
||||||
|
// Catches common formats: (555) 123-4567, 555-123-4567, 555.123.4567,
|
||||||
|
// +1 555 123 4567, 5551234567
|
||||||
|
_substitutePhones(text, identity) {
|
||||||
|
const replacements = [];
|
||||||
|
let result = text;
|
||||||
|
const phones = identity.phones || [];
|
||||||
|
|
||||||
|
for (const p of phones) {
|
||||||
|
if (!p.real || !p.substitute) continue;
|
||||||
|
|
||||||
|
// Normalize the real phone to just digits
|
||||||
|
const digits = p.real.replace(/\D/g, '');
|
||||||
|
if (digits.length < 7) continue;
|
||||||
|
|
||||||
|
// Build a regex that matches the digits in any common format
|
||||||
|
// For a number like 5551234567, match:
|
||||||
|
// 555-123-4567, (555) 123-4567, 555.123.4567, +1-555-123-4567, etc.
|
||||||
|
const d = digits.startsWith('1') && digits.length === 11
|
||||||
|
? digits.slice(1)
|
||||||
|
: digits;
|
||||||
|
|
||||||
|
if (d.length !== 10 && d.length !== 7) continue;
|
||||||
|
|
||||||
|
let pattern;
|
||||||
|
if (d.length === 10) {
|
||||||
|
const a = d.slice(0, 3), b = d.slice(3, 6), c = d.slice(6);
|
||||||
|
pattern =
|
||||||
|
'(?:\\+?1[\\s.-]?)?' +
|
||||||
|
'(?:' + esc(a) + '|\\(' + esc(a) + '\\))' +
|
||||||
|
'[\\s.\\-]?' +
|
||||||
|
esc(b) + '[\\s.\\-]?' + esc(c);
|
||||||
|
} else {
|
||||||
|
const b = d.slice(0, 3), c = d.slice(3);
|
||||||
|
pattern = esc(b) + '[\\s.\\-]?' + esc(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
const phoneRegex = new RegExp(pattern, 'g');
|
||||||
|
result = result.replace(phoneRegex, (matched) => {
|
||||||
|
replacements.push({
|
||||||
|
original: matched,
|
||||||
|
replaced: p.substitute,
|
||||||
|
category: 'phone',
|
||||||
|
pattern: 'smart-phone',
|
||||||
|
});
|
||||||
|
return p.substitute;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: result, replacements };
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----- File Paths -----
|
||||||
|
// Catches: /home/username, /Users/username, C:\Users\username
|
||||||
|
_substitutePaths(text, identity) {
|
||||||
|
const replacements = [];
|
||||||
|
let result = text;
|
||||||
|
const usernames = identity.usernames || [];
|
||||||
|
|
||||||
|
for (const u of usernames) {
|
||||||
|
if (!u.real || !u.substitute) continue;
|
||||||
|
|
||||||
|
// Unix paths: /home/username or /Users/username
|
||||||
|
const unixRegex = new RegExp(
|
||||||
|
'(/(?:home|Users)/)' + esc(u.real) + '(?=/|\\s|$|"|\')',
|
||||||
|
'g'
|
||||||
|
);
|
||||||
|
result = result.replace(unixRegex, (matched, prefix) => {
|
||||||
|
const sub = prefix + u.substitute;
|
||||||
|
replacements.push({
|
||||||
|
original: matched,
|
||||||
|
replaced: sub,
|
||||||
|
category: 'path',
|
||||||
|
pattern: 'smart-path',
|
||||||
|
});
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Windows paths: C:\Users\username
|
||||||
|
const winRegex = new RegExp(
|
||||||
|
'([A-Z]:\\\\Users\\\\)' + esc(u.real) + '(?=\\\\|\\s|$|"|\')',
|
||||||
|
'gi'
|
||||||
|
);
|
||||||
|
result = result.replace(winRegex, (matched, prefix) => {
|
||||||
|
const sub = prefix + u.substitute;
|
||||||
|
replacements.push({
|
||||||
|
original: matched,
|
||||||
|
replaced: sub,
|
||||||
|
category: 'path',
|
||||||
|
pattern: 'smart-path-win',
|
||||||
|
});
|
||||||
|
return sub;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: result, replacements };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function esc(str) {
|
||||||
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof globalThis !== 'undefined') {
|
||||||
|
globalThis.SmartPatterns = SmartPatterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SmartPatterns;
|
||||||
@@ -9,6 +9,7 @@ import api from './browser-polyfill.js';
|
|||||||
|
|
||||||
const KEYS = {
|
const KEYS = {
|
||||||
MAPPINGS: 'ss_mappings',
|
MAPPINGS: 'ss_mappings',
|
||||||
|
IDENTITY: 'ss_identity',
|
||||||
LOG: 'ss_activity_log',
|
LOG: 'ss_activity_log',
|
||||||
SETTINGS: 'ss_settings',
|
SETTINGS: 'ss_settings',
|
||||||
};
|
};
|
||||||
@@ -64,6 +65,25 @@ const Storage = {
|
|||||||
await this.saveMappings(filtered);
|
await this.saveMappings(filtered);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- Identity (Smart Patterns) ---
|
||||||
|
|
||||||
|
async getIdentity() {
|
||||||
|
const result = await api.storage.local.get(KEYS.IDENTITY);
|
||||||
|
return result[KEYS.IDENTITY] || {
|
||||||
|
emails: [],
|
||||||
|
names: [],
|
||||||
|
usernames: [],
|
||||||
|
phones: [],
|
||||||
|
catchAllEmail: '',
|
||||||
|
emailDomains: [],
|
||||||
|
enabled: { emails: true, names: true, usernames: true, phones: true, paths: true },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveIdentity(identity) {
|
||||||
|
await api.storage.local.set({ [KEYS.IDENTITY]: identity });
|
||||||
|
},
|
||||||
|
|
||||||
// --- Activity Log ---
|
// --- Activity Log ---
|
||||||
|
|
||||||
async getLog() {
|
async getLog() {
|
||||||
|
|||||||
@@ -242,6 +242,46 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Identity tab */
|
||||||
|
.id-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-section:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-sm {
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-hint {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mapping list */
|
/* Mapping list */
|
||||||
.add-mapping {
|
.add-mapping {
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
|
|||||||
+58
-2
@@ -29,13 +29,69 @@
|
|||||||
|
|
||||||
<!-- Tab Bar -->
|
<!-- Tab Bar -->
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<button class="tab active" data-tab="mappings">Mappings</button>
|
<button class="tab active" data-tab="identity">Identity</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>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Identity Tab (Smart Patterns) -->
|
||||||
|
<section class="tab-content active" id="tab-identity">
|
||||||
|
<p class="help-text">Tell Silent Send who you are. It auto-catches all variations.</p>
|
||||||
|
|
||||||
|
<div class="id-section">
|
||||||
|
<div class="id-label">Names</div>
|
||||||
|
<div class="id-row">
|
||||||
|
<input type="text" id="idFirstReal" placeholder="First name" class="input input-sm">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<input type="text" id="idFirstSub" placeholder="Fake first" class="input input-sm">
|
||||||
|
</div>
|
||||||
|
<div class="id-row">
|
||||||
|
<input type="text" id="idLastReal" placeholder="Last name" class="input input-sm">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<input type="text" id="idLastSub" placeholder="Fake last" class="input input-sm">
|
||||||
|
</div>
|
||||||
|
<div class="id-hint">Catches: first last, last first, possessives, case variants</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="id-section">
|
||||||
|
<div class="id-label">Email</div>
|
||||||
|
<div class="id-row">
|
||||||
|
<input type="email" id="idEmailReal" placeholder="you@gmail.com" class="input input-sm">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<input type="email" id="idEmailSub" placeholder="fake@example.com" class="input input-sm">
|
||||||
|
</div>
|
||||||
|
<div class="id-row">
|
||||||
|
<input type="email" id="idCatchAllEmail" placeholder="Catch-all email (for unknown addresses)" class="input" style="flex:1">
|
||||||
|
</div>
|
||||||
|
<div class="id-hint">Catches: any *@gmail.com, *@yahoo.com, *@outlook.com, etc.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="id-section">
|
||||||
|
<div class="id-label">Username / Computer</div>
|
||||||
|
<div class="id-row">
|
||||||
|
<input type="text" id="idUserReal" placeholder="jsmith" class="input input-sm">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<input type="text" id="idUserSub" placeholder="ademo" class="input input-sm">
|
||||||
|
</div>
|
||||||
|
<div class="id-hint">Catches: jsmith@hostname, /home/jsmith, ~jsmith, C:\Users\jsmith</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="id-section">
|
||||||
|
<div class="id-label">Phone</div>
|
||||||
|
<div class="id-row">
|
||||||
|
<input type="text" id="idPhoneReal" placeholder="(555) 123-4567" class="input input-sm">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<input type="text" id="idPhoneSub" placeholder="(555) 000-0000" class="input input-sm">
|
||||||
|
</div>
|
||||||
|
<div class="id-hint">Catches: all formats — (555) 123-4567, 555-123-4567, 555.123.4567</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" id="btnSaveIdentity" style="width:100%;margin-top:8px">Save Identity</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Mappings Tab -->
|
<!-- Mappings Tab -->
|
||||||
<section class="tab-content active" id="tab-mappings">
|
<section class="tab-content" id="tab-mappings">
|
||||||
<div class="add-mapping">
|
<div class="add-mapping">
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<input type="text" id="inputReal" placeholder="Real value (e.g. John Smith)" class="input">
|
<input type="text" id="inputReal" placeholder="Real value (e.g. John Smith)" class="input">
|
||||||
|
|||||||
+142
-14
@@ -1,9 +1,11 @@
|
|||||||
import SubstitutionEngine from '../lib/substitution-engine.js';
|
import SubstitutionEngine from '../lib/substitution-engine.js';
|
||||||
|
import SmartPatterns from '../lib/smart-patterns.js';
|
||||||
import Storage from '../lib/storage.js';
|
import Storage from '../lib/storage.js';
|
||||||
import api from '../lib/browser-polyfill.js';
|
import api from '../lib/browser-polyfill.js';
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
let mappings = [];
|
let mappings = [];
|
||||||
|
let identity = {};
|
||||||
let settings = {};
|
let settings = {};
|
||||||
|
|
||||||
// --- DOM refs ---
|
// --- DOM refs ---
|
||||||
@@ -13,10 +15,12 @@ const $$ = (sel) => document.querySelectorAll(sel);
|
|||||||
// --- Init ---
|
// --- Init ---
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
mappings = await Storage.getMappings();
|
mappings = await Storage.getMappings();
|
||||||
|
identity = await Storage.getIdentity();
|
||||||
settings = await Storage.getSettings();
|
settings = await Storage.getSettings();
|
||||||
|
|
||||||
renderMappings();
|
renderMappings();
|
||||||
renderActivity();
|
renderActivity();
|
||||||
|
loadIdentityForm();
|
||||||
updateStatusDot();
|
updateStatusDot();
|
||||||
|
|
||||||
$('#enableToggle').checked = settings.enabled;
|
$('#enableToggle').checked = settings.enabled;
|
||||||
@@ -57,6 +61,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
$('#btnReveal').classList.toggle('active', settings.revealMode);
|
$('#btnReveal').classList.toggle('active', settings.revealMode);
|
||||||
|
|
||||||
|
// Save identity
|
||||||
|
$('#btnSaveIdentity').addEventListener('click', saveIdentity);
|
||||||
|
|
||||||
// Add mapping
|
// Add mapping
|
||||||
$('#btnAdd').addEventListener('click', addMapping);
|
$('#btnAdd').addEventListener('click', addMapping);
|
||||||
$('#inputSub').addEventListener('keydown', (e) => {
|
$('#inputSub').addEventListener('keydown', (e) => {
|
||||||
@@ -79,6 +86,93 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Identity ---
|
||||||
|
function loadIdentityForm() {
|
||||||
|
const first = (identity.names || []).find(n => n.type === 'first');
|
||||||
|
const last = (identity.names || []).find(n => n.type === 'last');
|
||||||
|
const email = (identity.emails || [])[0];
|
||||||
|
const user = (identity.usernames || [])[0];
|
||||||
|
const phone = (identity.phones || [])[0];
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
$('#idFirstReal').value = first.real || '';
|
||||||
|
$('#idFirstSub').value = first.substitute || '';
|
||||||
|
}
|
||||||
|
if (last) {
|
||||||
|
$('#idLastReal').value = last.real || '';
|
||||||
|
$('#idLastSub').value = last.substitute || '';
|
||||||
|
}
|
||||||
|
if (email) {
|
||||||
|
$('#idEmailReal').value = email.real || '';
|
||||||
|
$('#idEmailSub').value = email.substitute || '';
|
||||||
|
}
|
||||||
|
$('#idCatchAllEmail').value = identity.catchAllEmail || '';
|
||||||
|
if (user) {
|
||||||
|
$('#idUserReal').value = user.real || '';
|
||||||
|
$('#idUserSub').value = user.substitute || '';
|
||||||
|
}
|
||||||
|
if (phone) {
|
||||||
|
$('#idPhoneReal').value = phone.real || '';
|
||||||
|
$('#idPhoneSub').value = phone.substitute || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveIdentity() {
|
||||||
|
const names = [];
|
||||||
|
const firstReal = $('#idFirstReal').value.trim();
|
||||||
|
const firstSub = $('#idFirstSub').value.trim();
|
||||||
|
if (firstReal && firstSub) {
|
||||||
|
names.push({ real: firstReal, substitute: firstSub, type: 'first' });
|
||||||
|
}
|
||||||
|
const lastReal = $('#idLastReal').value.trim();
|
||||||
|
const lastSub = $('#idLastSub').value.trim();
|
||||||
|
if (lastReal && lastSub) {
|
||||||
|
names.push({ real: lastReal, substitute: lastSub, type: 'last' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const emails = [];
|
||||||
|
const emailReal = $('#idEmailReal').value.trim();
|
||||||
|
const emailSub = $('#idEmailSub').value.trim();
|
||||||
|
if (emailReal && emailSub) {
|
||||||
|
emails.push({ real: emailReal, substitute: emailSub });
|
||||||
|
}
|
||||||
|
|
||||||
|
const usernames = [];
|
||||||
|
const userReal = $('#idUserReal').value.trim();
|
||||||
|
const userSub = $('#idUserSub').value.trim();
|
||||||
|
if (userReal && userSub) {
|
||||||
|
usernames.push({ real: userReal, substitute: userSub });
|
||||||
|
}
|
||||||
|
|
||||||
|
const phones = [];
|
||||||
|
const phoneReal = $('#idPhoneReal').value.trim();
|
||||||
|
const phoneSub = $('#idPhoneSub').value.trim();
|
||||||
|
if (phoneReal && phoneSub) {
|
||||||
|
phones.push({ real: phoneReal, substitute: phoneSub });
|
||||||
|
}
|
||||||
|
|
||||||
|
identity = {
|
||||||
|
names,
|
||||||
|
emails,
|
||||||
|
usernames,
|
||||||
|
phones,
|
||||||
|
catchAllEmail: $('#idCatchAllEmail').value.trim(),
|
||||||
|
emailDomains: identity.emailDomains || [],
|
||||||
|
enabled: identity.enabled || { emails: true, names: true, usernames: true, phones: true, paths: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
await Storage.saveIdentity(identity);
|
||||||
|
|
||||||
|
// Flash save button
|
||||||
|
const btn = $('#btnSaveIdentity');
|
||||||
|
btn.textContent = 'Saved!';
|
||||||
|
btn.style.background = '#059669';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = 'Save Identity';
|
||||||
|
btn.style.background = '';
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Add Mapping ---
|
// --- Add Mapping ---
|
||||||
async function addMapping() {
|
async function addMapping() {
|
||||||
const real = $('#inputReal').value.trim();
|
const real = $('#inputReal').value.trim();
|
||||||
@@ -191,6 +285,7 @@ async function renderActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Test Diff ---
|
// --- Test Diff ---
|
||||||
|
// Runs both smart patterns AND explicit mappings, shows combined result
|
||||||
function renderTestDiff() {
|
function renderTestDiff() {
|
||||||
const input = $('#testInput').value;
|
const input = $('#testInput').value;
|
||||||
const output = $('#diffOutput');
|
const output = $('#diffOutput');
|
||||||
@@ -202,22 +297,55 @@ function renderTestDiff() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { text, replacements } = SubstitutionEngine.substitute(input, mappings);
|
// Smart patterns first (broader catches), then explicit mappings (specific overrides)
|
||||||
const chunks = SubstitutionEngine.diff(input, text, mappings);
|
const smartResult = SmartPatterns.substitute(input, identity);
|
||||||
|
const explicitResult = SubstitutionEngine.substitute(smartResult.text, mappings);
|
||||||
|
|
||||||
output.innerHTML = chunks
|
const allReplacements = [...smartResult.replacements, ...explicitResult.replacements];
|
||||||
.map((chunk) => {
|
const finalText = explicitResult.text;
|
||||||
if (chunk.type === 'substituted') {
|
|
||||||
return `<span class="sub-highlight" title="Was: ${escapeHtml(chunk.original)}">${escapeHtml(chunk.replacement)}</span>`;
|
|
||||||
}
|
|
||||||
return escapeHtml(chunk.text);
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
stats.textContent =
|
// Simple diff: highlight differences
|
||||||
replacements.length > 0
|
if (finalText === input) {
|
||||||
? `${replacements.length} substitution${replacements.length !== 1 ? 's' : ''} would be made`
|
output.textContent = input;
|
||||||
: 'No substitutions detected';
|
stats.textContent = 'No substitutions detected';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a visual diff by running smart patterns on original to find positions
|
||||||
|
// For display, we re-run on the original to get positions
|
||||||
|
const smartPositions = findReplacementPositions(input, identity, mappings);
|
||||||
|
|
||||||
|
if (smartPositions.length === 0) {
|
||||||
|
output.textContent = finalText;
|
||||||
|
} else {
|
||||||
|
// Build highlighted output from the final text
|
||||||
|
// Simpler approach: show the final text with replaced values highlighted
|
||||||
|
let html = escapeHtml(finalText);
|
||||||
|
for (const r of allReplacements) {
|
||||||
|
const escapedReplaced = escapeHtml(r.replaced);
|
||||||
|
html = html.replace(
|
||||||
|
escapedReplaced,
|
||||||
|
`<span class="sub-highlight" title="Was: ${escapeHtml(r.original)} [${r.pattern || r.category}]">${escapedReplaced}</span>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
output.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
const smartCount = smartResult.replacements.length;
|
||||||
|
const explicitCount = explicitResult.replacements.length;
|
||||||
|
const parts = [];
|
||||||
|
if (smartCount > 0) parts.push(`${smartCount} smart`);
|
||||||
|
if (explicitCount > 0) parts.push(`${explicitCount} explicit`);
|
||||||
|
stats.textContent = `${allReplacements.length} substitution${allReplacements.length !== 1 ? 's' : ''} (${parts.join(', ')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findReplacementPositions(text, ident, maps) {
|
||||||
|
const positions = [];
|
||||||
|
const r1 = SmartPatterns.substitute(text, ident);
|
||||||
|
positions.push(...r1.replacements);
|
||||||
|
const r2 = SubstitutionEngine.substitute(r1.text, maps);
|
||||||
|
positions.push(...r2.replacements);
|
||||||
|
return positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Status Dot ---
|
// --- Status Dot ---
|
||||||
|
|||||||
Reference in New Issue
Block a user