export type LeakFinding = { category: 'credential' | 'email' | 'jwt' | 'token' | 'coordinates'; severity: 'warning' | 'info'; message: string; }; const TOKEN_PARAM_NAMES: ReadonlySet = new Set([ 'access_token', 'refresh_token', 'id_token', 'token', 'auth', 'authorization', 'auth_token', 'otp', 'one_time_code', 'reset', 'reset_token', 'password_reset_token', 'invite', 'invite_code', 'invitation_token', 'api_key', 'apikey', 'secret', 'client_secret', 'session', 'session_id', 'session_token', 'sessionid', 'signature', 'sig', 'private_token', 'share_token', 'confirmation_token', 'unlock_token', 'recovery_token', 'bearer', ]); const EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/; const JWT_RE = /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/; const MIN_TOKEN_LEN = 8; function pct(s: string): string { try { return decodeURIComponent(s); } catch { return s; } } function checkSensitiveData(raw: string, part: string): LeakFinding[] { const f: LeakFinding[] = []; const d = pct(raw); const e = d.match(EMAIL_RE); if (e) f.push({ category: 'email', severity: 'warning', message: `Email in ${part}: ${e[0]}` }); const j = d.match(JWT_RE); if (j) f.push({ category: 'jwt', severity: 'warning', message: `JWT in ${part} (${j[0].slice(0, 30)}…)` }); return f; } export function checkLinkLeaks(url: string): LeakFinding[] { const findings: LeakFinding[] = []; try { const u = new URL(url); if (u.password && u.password.length > 0) { findings.push({ category: 'credential', severity: 'warning', message: 'URL contains a password in userinfo.' }); } if (u.pathname && u.pathname !== '/') findings.push(...checkSensitiveData(u.pathname, 'path')); const tp: string[] = []; for (const [k, v] of u.searchParams) { const lk = k.toLowerCase(); if (TOKEN_PARAM_NAMES.has(lk) && pct(v).length >= MIN_TOKEN_LEN) tp.push(lk); findings.push(...checkSensitiveData(v, `query param '${k}'`)); } if (tp.length) findings.push({ category: 'token', severity: 'warning', message: `Token param(s): ${tp.join(', ')}` }); const lat = u.searchParams.get('lat'); const lon = u.searchParams.get('lon'); if (lat && /^[-+]?\d+\.\d{4,}$/.test(lat) && lon && /^[-+]?\d+\.\d{4,}$/.test(lon)) { findings.push({ category: 'coordinates', severity: 'info', message: `GPS coordinates: lat=${lat}, lon=${lon}` }); } if (u.hash) findings.push(...checkSensitiveData(u.hash.slice(1), 'fragment')); } catch {} return findings; } export function leakSummary(findings: LeakFinding[]): string { if (findings.length === 0) return ''; const lines = findings.map((f) => `• ${f.message}`); lines.unshift('This link may contain sensitive information:'); lines.push('', 'The receiver could see this data in the URL.'); return lines.join('\n'); }