/** * Turn a User-Agent string into a short "Browser · OS" label, so two sessions * of the same app can be told apart. Falls back to a trimmed raw value when it * can't be parsed, and null when there's nothing to show. * @param {string|null|undefined} ua * @returns {string|null} */ export function formatUserAgent(ua) { if (!ua) return null; const browser = /\bEdg\//.test(ua) ? 'Edge' : /\bOPR\/|\bOpera\b/.test(ua) ? 'Opera' : /\bChrome\//.test(ua) ? 'Chrome' : /\bFirefox\//.test(ua) ? 'Firefox' : /\bVersion\/.*\bSafari\//.test(ua) ? 'Safari' : null; const os = /\bAndroid\b/.test(ua) ? 'Android' : /\biPhone\b/.test(ua) ? 'iPhone' : /\biPad\b/.test(ua) ? 'iPad' : /\bMac OS X\b|\bMacintosh\b/.test(ua) ? 'macOS' : /\bWindows\b/.test(ua) ? 'Windows' : /\bLinux\b/.test(ua) ? 'Linux' : null; if (browser && os) return `${browser} · ${os}`; if (browser) return browser; // Not a browser: name the HTTP client behind the login (a script, a mobile // app's networking library) rather than showing a raw token, so it reads as // cleanly as a browser does. Unknown agents keep the trimmed raw fallback. const client = detectClient(ua); if (client) return client; if (os) return os; return ua.length > 40 ? `${ua.slice(0, 40)}…` : ua; } /** * The common non-browser HTTP clients, matched on their User-Agent token. * @param {string} ua * @returns {string|null} */ function detectClient(ua) { if (/\bcurl\//i.test(ua)) return 'curl'; if (/\bWget\b/i.test(ua)) return 'Wget'; if (/\bokhttp\//i.test(ua)) return 'OkHttp'; if (/\bpython-requests\/|\bPython-urllib\/|\bPython\//i.test(ua)) return 'Python'; if (/\bGo-http-client\//i.test(ua)) return 'Go'; if (/\baxios\//i.test(ua)) return 'axios'; if (/\bundici\b|\bnode-fetch\b|^node\b/i.test(ua)) return 'Node.js'; if (/\bDeno\//i.test(ua)) return 'Deno'; if (/\bBun\//i.test(ua)) return 'Bun'; if (/\bPostmanRuntime\//i.test(ua)) return 'Postman'; return null; }