feat: password import, proper noun detection, full README update

Import parser:
- Passwords now imported from Chrome/Firefox/Bitwarden/1Password CSVs
  as exact-match auto-redacted mappings (→ [REDACTED-PASSWORD-N])
- Catches passwords in any context, not just key=value patterns

Proper noun heuristic:
- Auto-detect scanner now catches capitalized words mid-sentence as
  potential names, company names, or project names
- Filters against 200+ common English words, programming terms, days,
  months to reduce false positives
- Added to both content.js (page world) and auto-detect.js (popup)

README:
- Documented proper noun detection with examples
- Documented bulk import with all supported formats
- Added Sync features section (auto sync, conflict resolution,
  version history, connected devices)
- Added Organization/Team section with policy format
- Added tamper protection documentation
- Added Legal section with liability analysis
- Updated architecture with all new modules

https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
Claude
2026-03-26 20:05:17 +00:00
parent d969c7b375
commit 7c968e1461
4 changed files with 356 additions and 6 deletions
+113
View File
@@ -346,6 +346,114 @@
const CONTEXT_WORDS_RE = /\b(?:born|birthday|dob|birth|passport|license|driver|ssn|social\s*security|address|zip|postal|date\s+of\s+birth)\b/i;
// Common English words that are capitalized but aren't proper nouns.
// Used by the proper noun heuristic to reduce false positives.
const COMMON_CAPITALIZED = new Set([
'the', 'a', 'an', 'and', 'or', 'but', 'if', 'then', 'else', 'when',
'at', 'by', 'for', 'with', 'about', 'against', 'between', 'through',
'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up',
'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further',
'then', 'once', 'here', 'there', 'all', 'each', 'every', 'both',
'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not',
'only', 'own', 'same', 'so', 'than', 'too', 'very', 'can', 'will',
'just', 'should', 'now', 'also', 'into', 'could', 'would', 'may',
'might', 'shall', 'must', 'need', 'have', 'has', 'had', 'do', 'does',
'did', 'be', 'is', 'am', 'are', 'was', 'were', 'been', 'being',
'get', 'got', 'make', 'made', 'go', 'went', 'gone', 'take', 'took',
'come', 'came', 'see', 'saw', 'know', 'knew', 'think', 'thought',
'say', 'said', 'tell', 'told', 'give', 'gave', 'find', 'found',
'want', 'let', 'put', 'set', 'run', 'keep', 'try', 'start', 'turn',
'show', 'hear', 'play', 'move', 'live', 'believe', 'bring', 'happen',
'write', 'provide', 'sit', 'stand', 'lose', 'pay', 'meet', 'include',
'continue', 'learn', 'change', 'lead', 'understand', 'watch', 'follow',
'stop', 'create', 'speak', 'read', 'allow', 'add', 'spend', 'grow',
'open', 'walk', 'win', 'offer', 'remember', 'love', 'consider', 'appear',
'buy', 'wait', 'serve', 'die', 'send', 'expect', 'build', 'stay',
'fall', 'cut', 'reach', 'kill', 'remain', 'suggest', 'raise', 'pass',
'sell', 'require', 'report', 'decide', 'pull', 'develop', 'note',
'however', 'because', 'although', 'since', 'while', 'where', 'which',
'what', 'who', 'how', 'why', 'this', 'that', 'these', 'those',
'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her',
'us', 'them', 'my', 'your', 'his', 'its', 'our', 'their',
'new', 'old', 'big', 'small', 'long', 'short', 'good', 'bad',
'great', 'little', 'right', 'left', 'first', 'last', 'next',
'sure', 'like', 'well', 'back', 'still', 'even', 'much', 'many',
// Programming/tech words that appear capitalized
'string', 'number', 'boolean', 'object', 'array', 'function', 'class',
'type', 'error', 'null', 'undefined', 'true', 'false', 'return',
'import', 'export', 'default', 'const', 'let', 'var', 'async', 'await',
'try', 'catch', 'throw', 'finally', 'switch', 'case', 'break',
'note', 'example', 'warning', 'important', 'todo', 'fixme', 'hack',
'step', 'option', 'result', 'value', 'key', 'data', 'info',
'file', 'code', 'test', 'debug', 'config', 'setup', 'update',
// Common sentence starters that aren't names
'please', 'thanks', 'hello', 'hi', 'hey', 'dear', 'sincerely',
'regards', 'best', 'cheers', 'sorry', 'yes', 'no', 'ok', 'okay',
// Days and months (not PPI)
'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
'january', 'february', 'march', 'april', 'may', 'june', 'july',
'august', 'september', 'october', 'november', 'december',
'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun',
'jan', 'feb', 'mar', 'apr', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec',
]);
/**
* Detect proper nouns (potential names, company names, project names)
* that aren't configured in identity. Uses capitalization heuristics:
* - Capitalized words not at the start of a sentence
* - Multi-word capitalized sequences (e.g. "Acme Corp", "Project Atlas")
* - Filters out common English words and programming terms
*/
function detectProperNouns(text, configured) {
const findings = [];
// Match capitalized words that aren't at the very start of the text
// and aren't after a period/newline (sentence start)
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;
// Check if this is at the start of a sentence
const before = text.slice(Math.max(0, m.index - 2), m.index);
const isSentenceStart = m.index === 0 || /[.!?\n]\s*$/.test(before);
// Split into individual words and check each
const words = fullMatch.split(/\s+/);
const properWords = words.filter(w =>
w.length >= 3 &&
!COMMON_CAPITALIZED.has(w.toLowerCase()) &&
!configured.has(w.toLowerCase())
);
if (properWords.length === 0) continue;
// Single capitalized word at sentence start = likely not a proper noun
if (isSentenceStart && properWords.length === 1 && words.length === 1) continue;
// Multi-word capitalized sequence is likely a proper noun
// Single capitalized word mid-sentence is likely a proper noun
const value = properWords.join(' ');
if (value.length >= 3 && !configured.has(value.toLowerCase())) {
findings.push({
name: 'Possible Name/Org',
value,
hint: 'Capitalized word — could be a name, company, or project',
category: 'name',
});
}
}
// Deduplicate
const seen = new Set();
return findings.filter(f => {
if (seen.has(f.value)) return false;
seen.add(f.value);
return true;
});
}
function autoDetectPPI(text, ident) {
if (!text || text.length < 5) return [];
const hasContext = CONTEXT_WORDS_RE.test(text);
@@ -374,6 +482,11 @@
}
}
// Proper noun heuristic — catch names, company names, project names
// that aren't configured in identity
const properNouns = detectProperNouns(text, configured);
findings.push(...properNouns);
// Deduplicate by value
const seen = new Set();
return findings.filter(f => {
+86 -3
View File
@@ -193,21 +193,104 @@ const AutoDetect = {
}
}
// Proper noun heuristic — catch names, company names, project names
const properNouns = this._detectProperNouns(text, configured);
findings.push(...properNouns);
// Deduplicate overlapping matches
findings.sort((a, b) => a.index - b.index);
findings.sort((a, b) => (a.index || 0) - (b.index || 0));
const deduped = [];
let lastEnd = -1;
for (const f of findings) {
if (f.index >= lastEnd) {
const idx = f.index || 0;
if (idx >= lastEnd) {
deduped.push(f);
lastEnd = f.index + f.value.length;
lastEnd = idx + f.value.length;
}
}
return deduped;
},
/**
* Detect capitalized words mid-sentence that might be proper nouns
* (names, company names, project names) not in the configured set.
*/
_detectProperNouns(text, configured) {
const findings = [];
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 &&
!COMMON_WORDS.has(w.toLowerCase()) &&
!configured.has(w.toLowerCase())
);
if (properWords.length === 0) continue;
if (isSentenceStart && properWords.length === 1 && words.length === 1) continue;
const value = properWords.join(' ');
if (value.length >= 3 && !configured.has(value.toLowerCase())) {
findings.push({
name: 'Possible Name/Org',
value,
hint: 'Capitalized word — could be a name, company, or project',
category: 'name',
});
}
}
const seen = new Set();
return findings.filter(f => {
if (seen.has(f.value)) return false;
seen.add(f.value);
return true;
});
},
};
// Common English words to exclude from proper noun detection
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',
'new', 'now', 'old', 'see', 'way', 'who', 'did', 'get', 'let', 'say',
'she', 'too', 'use', 'also', 'back', 'been', 'call', 'came', 'come',
'could', 'each', 'even', 'find', 'from', 'give', 'good', 'great',
'have', 'here', 'high', 'into', 'just', 'keep', 'know', 'last', 'like',
'live', 'long', 'look', 'made', 'make', 'many', 'more', 'most', 'much',
'must', 'name', 'next', 'only', 'over', 'part', 'people', 'place',
'same', 'show', 'side', 'since', 'some', 'still', 'such', 'take',
'tell', 'than', 'that', 'them', 'then', 'there', 'these', 'they',
'this', 'time', 'turn', 'used', 'very', 'want', 'well', 'were',
'what', 'when', 'where', 'which', 'while', 'will', 'with', 'work',
'would', 'year', 'your', 'about', 'after', 'again', 'being', 'between',
'both', 'before', 'down', 'during', 'first', 'found', 'group',
'however', 'important', 'large', 'later', 'little', 'never',
'number', 'other', 'point', 'right', 'small', 'state', 'thing',
'think', 'those', 'three', 'through', 'under', 'until', 'water',
'world', 'write', 'might', 'should', 'because', 'although',
// Programming / tech terms
'string', 'number', 'boolean', 'object', 'array', 'function', 'class',
'type', 'error', 'null', 'undefined', 'true', 'false', 'return',
'import', 'export', 'default', 'const', 'async', 'await',
'note', 'example', 'warning', 'step', 'option', 'result', 'value',
'key', 'data', 'info', 'file', 'code', 'test', 'debug', 'config',
'setup', 'update', 'please', 'thanks', 'hello', 'sorry',
// Days and months
'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
'january', 'february', 'march', 'april', 'may', 'june', 'july',
'august', 'september', 'october', 'november', 'december',
]);
if (typeof globalThis !== 'undefined') {
globalThis.AutoDetect = AutoDetect;
}
+42 -3
View File
@@ -13,9 +13,9 @@
* Imports as identity fields with blank substitutes so the user
* can see what needs mapping and fill in fakes.
*
* 3. Chrome password CSV export — extracts usernames, names, URLs
* 3. Chrome password CSV export — extracts usernames, names, URLs, passwords
* Columns: name, url, username, password, note
* Passwords are NEVER imported. Only usernames/emails/URLs.
* Passwords are imported as auto-redacted mappings (e.g. → [REDACTED-PASSWORD-1]).
*
* 4. Firefox password CSV export — similar to Chrome
* Columns: url, username, password, ...
@@ -139,7 +139,10 @@ const ImportParser = {
/**
* Parse Chrome/Firefox password CSV export.
* NEVER imports passwords — only usernames, emails, and domains.
* Imports usernames, emails, domains, AND passwords.
* Passwords are imported as mappings with auto-generated redaction
* substitutes (e.g. "[REDACTED-PASSWORD-1]") so they get caught
* if pasted into any context — not just key=value patterns.
*/
parsePasswordCSV(text) {
const result = this._empty();
@@ -148,12 +151,15 @@ const ImportParser = {
const headers = this._splitCSVLine(lines[0], ',').map(h => h.trim().toLowerCase());
const usernameIdx = headers.findIndex(h => h === 'username' || h === 'login_username' || h === 'user');
const passwordIdx = headers.findIndex(h => h === 'password' || h === 'login_password');
const urlIdx = headers.findIndex(h => h === 'url' || h === 'login_uri' || h === 'origin' || h === 'web site');
const nameIdx = headers.findIndex(h => h === 'name' || h === 'title');
const seenEmails = new Set();
const seenUsernames = new Set();
const seenDomains = new Set();
const seenPasswords = new Set();
let passwordCount = 0;
for (let i = 1; i < lines.length; i++) {
const cols = this._splitCSVLine(lines[i], ',');
@@ -172,6 +178,22 @@ const ImportParser = {
}
}
// Extract password — import as a redacted mapping
if (passwordIdx >= 0 && cols[passwordIdx]) {
const password = cols[passwordIdx].trim();
// Skip very short or empty passwords, and deduplicate
if (password && password.length >= 4 && !seenPasswords.has(password)) {
seenPasswords.add(password);
passwordCount++;
result.mappings.push({
real: password,
substitute: `[REDACTED-PASSWORD-${passwordCount}]`,
category: 'password',
caseSensitive: true,
});
}
}
// Extract domain from URL
if (urlIdx >= 0 && cols[urlIdx]) {
try {
@@ -202,9 +224,12 @@ const ImportParser = {
const headers = this._splitCSVLine(lines[0], ',').map(h => h.trim().toLowerCase());
const usernameIdx = headers.findIndex(h => h.includes('username'));
const passwordIdx = headers.findIndex(h => h.includes('password'));
const uriIdx = headers.findIndex(h => h.includes('uri') || h.includes('url'));
const seen = new Set();
const seenPasswords = new Set();
let passwordCount = 0;
for (let i = 1; i < lines.length; i++) {
const cols = this._splitCSVLine(lines[i], ',');
@@ -221,6 +246,20 @@ const ImportParser = {
}
}
if (passwordIdx >= 0 && cols[passwordIdx]) {
const pw = cols[passwordIdx].trim();
if (pw && pw.length >= 4 && !seenPasswords.has(pw)) {
seenPasswords.add(pw);
passwordCount++;
result.mappings.push({
real: pw,
substitute: `[REDACTED-PASSWORD-${passwordCount}]`,
category: 'password',
caseSensitive: true,
});
}
}
if (uriIdx >= 0 && cols[uriIdx]) {
try {
const domain = new URL(cols[uriIdx].trim()).hostname;