fix: date false-positive, partial-word highlights, cross-browser sync

Bug fixes:
- Date (possible DOB) pattern now requires context words (born, birthday,
  dob, etc.) before firing — prevents spurious warnings on page-load API
  calls that happen to contain ISO dates in conversation history.
- Highlight regex now uses word boundaries (\b) so short substitute values
  (e.g. "aud") no longer match inside unrelated words like "Claude".
- Both TreeWalkers in content.js now skip the extension's own UI elements
  (.ss-autodetect-warning, .ss-presend-warning, .ss-reveal-badge) to
  prevent the highlight API from marking text in the extension's banners.

Settings sync:
- New src/lib/sync.js: exportSyncCode / importSyncCode (base64 JSON) for
  manual copy-paste across any browser combination. Newest lastModified
  timestamp wins; force flag available to override.
- browser.storage.sync support: when "Browser account sync" is enabled the
  extension automatically pushes/pulls via Firefox Sync or Chrome account,
  chunked to stay within per-item quota limits.
- storage.js now writes ss_lastModified on every save so conflict resolution
  has an accurate timestamp.
- service-worker.js listens for both local and sync storage changes to keep
  all copies in sync.
- New "Sync Between Browsers" section in options.html with Generate/Copy/
  Import Sync Code UI and the browser sync toggle.

https://claude.ai/code/session_01TKpSR9M8JgHLXCp5CeDsQP
This commit is contained in:
Claude
2026-03-26 14:13:22 +00:00
parent 2e79e02bd3
commit 84f6e675db
6 changed files with 340 additions and 12 deletions
+13 -5
View File
@@ -327,7 +327,7 @@
hint: 'GPS coordinates — pinpoints a location', cat: 'address' },
// Personal
{ name: 'Date (possible DOB)', re: /\b(?:(?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01])[-/](?:19|20)\d{2}|(?:19|20)\d{2}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01]))\b/g,
hint: 'Date — could be a birthday', cat: 'personal' },
hint: 'Date — could be a birthday', cat: 'personal', contextRequired: true },
{ name: 'EIN / Tax ID', re: /\b\d{2}-\d{7}\b/g,
hint: 'Could be a tax ID', cat: 'document' },
// Paths not caught by smart patterns
@@ -344,8 +344,11 @@
hint: 'Env variable with personal data', cat: 'env' },
];
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;
function autoDetectPPI(text, ident) {
if (!text || text.length < 5) return [];
const hasContext = CONTEXT_WORDS_RE.test(text);
// Build skip set from configured values
const configured = new Set();
@@ -360,6 +363,7 @@
const findings = [];
for (const pat of PPI_PATTERNS) {
if (pat.contextRequired && !hasContext) continue;
pat.re.lastIndex = 0;
let m;
while ((m = pat.re.exec(text)) !== null) {
@@ -796,7 +800,7 @@
acceptNode(node) {
const parent = node.parentElement;
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
if (parent?.classList?.contains('ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
if (parent?.closest?.('.ss-autodetect-warning, .ss-presend-warning, .ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
@@ -846,7 +850,7 @@
acceptNode(node) {
const parent = node.parentElement;
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
if (parent?.classList?.contains('ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
if (parent?.closest?.('.ss-autodetect-warning, .ss-presend-warning, .ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
@@ -857,9 +861,13 @@
if (!text || text.length < MIN_STRING_LENGTH) continue;
for (const p of pairs) {
const escaped = esc(settings.revealMode ? p.to : p.from);
const searchTerm = settings.revealMode ? p.to : p.from;
const regex = new RegExp(escaped, p.caseSensitive ? 'g' : 'gi');
const escaped = esc(searchTerm);
// Add word boundaries when the term starts/ends with word chars to
// prevent partial-word matches (e.g. "aud" inside "Claude")
const bStart = /^\w/.test(searchTerm) ? '\\b' : '';
const bEnd = /\w$/.test(searchTerm) ? '\\b' : '';
const regex = new RegExp(bStart + escaped + bEnd, p.caseSensitive ? 'g' : 'gi');
let match;
while ((match = regex.exec(text)) !== null) {