1// Shared browser/server layer. No network, logs, persistent state or reverse map. 2// Complements the reviewed baseline; it cannot promise complete anonymisation. 3import * as baseline from './privacy.mjs'; 4export const VERSION='2026-09-25-minimisation-2'; 5const omitted='[Sensitive details omitted. Please describe the immigration question without sensitive personal details.]'; 6export function normalise(text){return String(text||'').normalize('NFKC').replace(/[\u200B-\u200D\u2060\uFEFF\u202A-\u202E\u2066-\u2069]/g,'')} 7export function emailIn(text){return baseline.emailIn(normalise(text))} 8// Match personal disclosures, not topic words: general sourced questions and 9// requests for a specialist must remain usable. This is still best-effort. 10const personalIdentity=/\b(?:i am|i['â]m|ich bin|my (?:wife|husband|partner|child|son|daughter) is|mein(?:e)? (?:frau|mann|partner(?:in)?|kind|sohn|tochter) ist)\s+(?:(?:a|an|ein|eine)\s+)?(?:muslim(?:a|in|isch)?|christian|christ(?:in|lich)?|jewish|jude|jüdin|jüdisch|hindu|buddhist(?:in|isch)?|sikh|atheist(?:in|isch)?|gay|lesbian|lesbisch|schwul|bisexual|bisexuell|homosexual|homosexuell|queer|transgender|transsexuell)\b/iu; 11const personalHealth=/\b(?:(?:i (?:take|use|inject|am taking)|i['â]m taking)\s+(?:(?:prescribed|daily)\s+)?insulin|ich (?:nehme|spritze|benutze)\s+(?:täglich\s+)?insulin|(?:i have|i['â]ve got|i suffer from|ich habe|ich leide an)\s+(?:diabetes|epilepsy|epilepsie|depression|ptsd|ptbs))\b/iu; 12export function sensitive(text){const t=normalise(text);return baseline.sensitive(t)||personalIdentity.test(t)||personalHealth.test(t)||/\b(?:i am|i['â]m|ich bin)\s+(?:pregnant|schwanger|hiv.positive)|\b(?:bei mir wurde|my diagnosis|meine diagnose|meine religion|my religion is|my sexual orientation)\b/i.test(t)} 13function cardNumber(s){const digits=s.replace(/\D/g,'');if(digits.length<13||digits.length>19)return false;let sum=0,twice=false;for(let i=digits.length-1;i>=0;i--){let n=Number(digits[i]);if(twice){n*=2;if(n>9)n-=9}sum+=n;twice=!twice}return sum%10===0} 14export function redact(text){let t=normalise(text) 15 .replace(/[\p{L}\p{N}._%+-]+@[\p{L}\p{N}.-]+\.[\p{L}]{2,}/gu,'[email withheld]') 16 .replace(/\b[\w.+-]+\s*(?:\[at\]|\(at\)|\sat\s)\s*[\w-]+(?:\s*(?:\[dot\]|\(dot\)|\sdot\s)\s*[\w-]+|\.[\w-]+)+\b/gi,'[email withheld]') 17 .replace(/\b(?:date of birth|dob|born on|geburtsdatum|geboren am)\s*(?:is|ist|:)?\s*(?:\d{4}-\d{1,2}-\d{1,2}|\d{1,2}[./-]\d{1,2}[./-]\d{2,4}|\d{1,2}\.?\s+[\p{L}]+\s+\d{4}|[\p{L}]+\s+\d{1,2},?\s+\d{4})/giu,'[date of birth withheld]') 18 .replace(/\b(?:full name|first name|last name|surname|name|vorname|nachname)\s*:\s*[\p{L}'â-]+(?:[ \t]+[\p{L}'â-]+){0,2}/giu,'[name withheld]') 19 .replace(/\b(?:document (?:number|id)|national id|identity card(?: number)?|personalausweis(?:nummer)?|versicherungsnummer|sozialversicherungsnummer|steueridentifikationsnummer|eAT(?: number| nummer)?)\s*(?:is|ist|:|#)?\s*[A-Z0-9][A-Z0-9/-]{4,}\b/gi,'[identifier withheld]') 20 .replace(/\b\d{1,5}\s+[\p{L}'â-]+(?:\s+[\p{L}'â-]+){0,3}\s+(?:street|road|avenue|lane|drive|st\.?|rd\.?)\b(?:[ ,]+(?:apt|apartment|flat|unit)\.?\s*[\w-]+)?/giu,'[street address withheld]') 21 .replace(/\b0(?:1[5-7]\d|[2-9]\d)[\d ()/-]{5,}\d\b/g,'[phone withheld]') 22 .replace(/\b(?:\d[ -]?){12,18}\d\b/g,m=>cardNumber(m)?'[payment card withheld]':m) 23 .replace(/\b(?:password|passwort|api[ _-]?key|access[ _-]?token)\s*[:=]\s*[^\s,;]+/gi,'[secret withheld]') 24 .replace(/\bBearer\s+[A-Za-z0-9._~+/-]{12,}=*/g,'[access token withheld]'); 25 return baseline.redact(t); 26} 27export function safeInput(text){return sensitive(text)?omitted:redact(text)} 28export function safeFacts(facts){return Object.fromEntries(Object.entries(baseline.safeFacts(facts)).map(([k,v])=>[k,typeof v==='string'?safeInput(v):v]))} 29export function browserMessage(text,phase){ 30 if(sensitive(text))return {blocked:true}; 31 // Only an explicitly requested, standalone contact address crosses unmasked. 32 // The server encrypts this separately and removes it from transcripts/models. 33 const contact=['email','delay_email','queued','human'].includes(phase)&&text.length<=254&&/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text); 34 return {blocked:false,message:contact?normalise(text):safeInput(text),display:contact?'[Contact email withheld]':safeInput(text),contact}; 35}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.