express-useragent
Version:
JS Library & ExpressJS user-agent middleware exposing
1 lines • 70.8 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/express-useragent.ts"],"sourcesContent":["import type { NextFunction, Request, Response } from 'express';\nimport type { IncomingHttpHeaders } from 'http';\nimport { UserAgent } from './express-useragent';\nimport type { AgentDetails } from './express-useragent';\n\nexport type { AgentDetails, HeadersLike, ClientHints, ClientHintBrand } from './express-useragent';\nexport { UserAgent } from './express-useragent';\n\n// Declaration merging for Express Request\n// This allows TypeScript users to access req.useragent without type errors\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n useragent?: AgentDetails;\n }\n }\n}\n\nexport interface UserAgentAugmentedRequest extends Request {\n useragent?: ReturnType<UserAgent['parse']>;\n headers: IncomingHttpHeaders & Record<string, string | string[] | undefined>;\n}\n\nexport const useragent = new UserAgent();\n\nexport type UserAgentMiddleware = (req: Request, res: Response, next: NextFunction) => void;\n\nexport const express = (): UserAgentMiddleware => {\n return (req: UserAgentAugmentedRequest, res: Response, next: NextFunction) => {\n const headers = req.headers || {};\n const resolveHeader = (value: string | string[] | undefined): string => {\n if (Array.isArray(value)) {\n return value.join(' ');\n }\n return value ?? '';\n };\n\n const uaHeader = resolveHeader(headers['user-agent']);\n const ucHeader = resolveHeader(headers['x-ucbrowser-ua']);\n const source = (ucHeader || uaHeader || 'unknown').trim() || 'unknown';\n\n const parser = new UserAgent().hydrateFromHeaders(source, headers);\n parser.testNginxGeoIP(headers);\n // middleware duplicates tests to match legacy behaviour\n parser.testBot();\n parser.testMobile();\n parser.testAndroidTablet();\n parser.testTablet();\n parser.testCompatibilityMode();\n parser.testSilk();\n parser.testKindleFire();\n parser.testWechat();\n\n req.useragent = parser.Agent;\n res.locals.useragent = parser.Agent;\n\n next();\n };\n};\nexport const useragentMiddleware = express;\n\n// Export a default instance for easier usage\n// Also attach the express middleware and UserAgent class to the default export\n// to maintain compatibility with documented ESM usage (issue #177)\nconst defaultExport = Object.assign(useragent, {\n express,\n UserAgent,\n});\n\nexport default defaultExport;\n","import type { IncomingHttpHeaders } from 'http';\n\n/**\n * Represents a single brand entry from User-Agent Client Hints\n * @see https://wicg.github.io/ua-client-hints/\n */\nexport interface ClientHintBrand {\n brand: string;\n version: string;\n}\n\n/**\n * Parsed User-Agent Client Hints data from HTTP headers\n * Maps to the NavigatorUAData interface exposed by browsers\n */\nexport interface ClientHints {\n brands: ClientHintBrand[];\n mobile: boolean;\n platform: string;\n platformVersion: string;\n architecture: string;\n bitness: string;\n model: string;\n fullVersionList: ClientHintBrand[];\n}\n\nconst BOTS = [\n '\\\\+https:\\\\/\\\\/developers.google.com\\\\/\\\\+\\\\/web\\\\/snippet\\\\/',\n 'ad\\\\smonitoring',\n 'adsbot',\n 'apex',\n 'applebot',\n 'archive.org_bot',\n 'baiduspider',\n 'bingbot',\n 'chromeheadless',\n 'cloudflare',\n 'cloudinary',\n 'crawler',\n 'curl',\n 'discordbot',\n 'duckduckbot',\n 'embedly',\n 'exabot',\n 'facebookexternalhit',\n 'facebot',\n 'flipboard',\n 'google',\n 'googlebot',\n 'gsa-crawler',\n 'gurujibot',\n 'guzzlehttp',\n 'heritrix',\n 'ia_archiver',\n 'insights',\n 'linkedinbot',\n 'ltx71',\n 'mediapartners',\n 'msnbot',\n 'odklbot',\n 'phantom\\\\.js',\n 'phantomjs',\n 'pingdom',\n 'pinterest',\n 'python',\n 'rtlnieuws',\n 'skypeuripreview',\n 'slackbot',\n 'slurp',\n 'spbot',\n 'telegrambot',\n 'test\\\\scertificate',\n 'testing',\n 'tiabot',\n 'tumblr ',\n 'twitterbot',\n 'vkshare',\n 'web\\\\sscraper',\n 'wget',\n 'yandexbot',\n 'whatsapp',\n 'orangebot',\n 'smtbot',\n 'qwantify',\n 'mj12bot',\n 'ahrefsbot',\n 'seznambot',\n 'panscient\\\\.com',\n 'duckduckgo-favicons-bot',\n 'uptimerobot',\n 'semrushbot',\n 'postman',\n 'dotbot',\n 'zoominfobot',\n 'ifttt',\n 'sogou',\n 'ru_bot',\n 'researchscan',\n 'nimbostratus-bot',\n 'slack-imgproxy',\n 'node-superagent',\n 'go-http-client',\n 'jersey',\n 'dataprovider.com',\n 'github-camo',\n 'dispatch',\n 'checkmarknetwork',\n 'screaming frog',\n 'whatweb',\n 'daum',\n 'netcraftsurveyagent',\n 'mojeekbot',\n 'surdotlybot',\n 'springbot',\n] as const;\n\nconst IS_BOT_REGEXP = new RegExp(`(${BOTS.join('|')})`, 'i');\nconst SILK_REGEXP = /silk/i;\nconst SILK_ACCELERATED_REGEXP = /Silk-Accelerated=true/i;\nconst SMART_TV_REGEXP = /smart-tv|smarttv|googletv|appletv|hbbtv|pov_tv|netcast.tv/i;\nconst ANDROID_TABLET_REGEXP = /mobile/i;\nconst MOBILE_REGEXP = /mobile|^ios-/i;\nconst DALVIK_REGEXP = /dalvik/i;\nconst IOS_SCALE_REGEXP = /scale/i;\nconst OKHTTP_REGEXP = /okhttp/i;\nconst WEBKIT_REGEXP = /applewebkit/i;\nconst WECHAT_REGEXP = /micromessenger/i;\n\nexport interface AgentDetails extends Record<string, unknown> {\n isYaBrowser: boolean;\n isAuthoritative: boolean;\n isMobile: boolean;\n isMobileNative: boolean;\n isTablet: boolean;\n isiPad: boolean;\n isiPod: boolean;\n isiPhone: boolean;\n isiPhoneNative: boolean;\n isAndroid: boolean;\n isAndroidNative: boolean;\n isBlackberry: boolean;\n isOpera: boolean;\n isIE: boolean;\n isEdge: boolean;\n isIECompatibilityMode: boolean;\n isSafari: boolean;\n isFirefox: boolean;\n isWebkit: boolean;\n isChrome: boolean;\n isKonqueror: boolean;\n isOmniWeb: boolean;\n isSeaMonkey: boolean;\n isFlock: boolean;\n isAmaya: boolean;\n isPhantomJS: boolean;\n isEpiphany: boolean;\n isDesktop: boolean;\n isWindows: boolean;\n isLinux: boolean;\n isLinux64: boolean;\n isMac: boolean;\n isChromeOS: boolean;\n isBada: boolean;\n isSamsung: boolean;\n isRaspberry: boolean;\n isBot: boolean;\n botName: string;\n isCurl: boolean;\n isAndroidTablet: boolean;\n isWinJs: boolean;\n isKindleFire: boolean;\n isSilk: boolean;\n isCaptive: boolean;\n isSmartTV: boolean;\n isUC: boolean;\n isFacebook: boolean;\n isAlamoFire: boolean;\n isElectron: boolean;\n silkAccelerated: boolean;\n browser: string;\n version: string | number;\n os: string;\n platform: string;\n geoIp: Record<string, string | string[]>;\n source: string;\n isWechat: boolean;\n isWindowsPhone: boolean;\n electronVersion: string;\n SilkAccelerated?: boolean;\n /** DuckDuckGo browser detected via Client Hints brand or WebKit UA pattern */\n isDuckDuckGo: boolean;\n /** Parsed User-Agent Client Hints when available */\n clientHints: ClientHints | null;\n}\n\nexport type HeadersLike = Partial<Record<string, string | string[] | undefined>>;\n\nconst DEFAULT_AGENT: AgentDetails = {\n isYaBrowser: false,\n isAuthoritative: true,\n isMobile: false,\n isMobileNative: false,\n isTablet: false,\n isiPad: false,\n isiPod: false,\n isiPhone: false,\n isiPhoneNative: false,\n isAndroid: false,\n isAndroidNative: false,\n isBlackberry: false,\n isOpera: false,\n isIE: false,\n isEdge: false,\n isIECompatibilityMode: false,\n isSafari: false,\n isFirefox: false,\n isWebkit: false,\n isChrome: false,\n isKonqueror: false,\n isOmniWeb: false,\n isSeaMonkey: false,\n isFlock: false,\n isAmaya: false,\n isPhantomJS: false,\n isEpiphany: false,\n isDesktop: false,\n isWindows: false,\n isLinux: false,\n isLinux64: false,\n isMac: false,\n isChromeOS: false,\n isBada: false,\n isSamsung: false,\n isRaspberry: false,\n isBot: false,\n botName: '',\n isCurl: false,\n isAndroidTablet: false,\n isWinJs: false,\n isKindleFire: false,\n isSilk: false,\n isCaptive: false,\n isSmartTV: false,\n isUC: false,\n isFacebook: false,\n isAlamoFire: false,\n isElectron: false,\n silkAccelerated: false,\n browser: 'unknown',\n version: 'unknown',\n os: 'unknown',\n platform: 'unknown',\n isWechat: false,\n isWindowsPhone: false,\n SilkAccelerated: false,\n geoIp: {},\n source: '',\n electronVersion: '',\n isDuckDuckGo: false,\n clientHints: null,\n};\n\nfunction createDefaultAgent(): AgentDetails {\n return {\n ...DEFAULT_AGENT,\n geoIp: {},\n source: '',\n electronVersion: '',\n botName: '',\n clientHints: null,\n };\n}\n\ninterface ProductToken {\n name: string;\n version: string;\n}\n\nconst isProductTokenChar = (charCode: number): boolean =>\n (charCode >= 48 && charCode <= 57) ||\n (charCode >= 65 && charCode <= 90) ||\n (charCode >= 97 && charCode <= 122) ||\n charCode === 45 ||\n charCode === 46 ||\n charCode === 95;\n\nconst isDigit = (charCode: number): boolean => charCode >= 48 && charCode <= 57;\n\nconst readProductToken = (source: string, startIndex = 0): ProductToken | null => {\n const slashIndex = source.indexOf('/', startIndex);\n if (slashIndex <= startIndex) {\n return null;\n }\n\n for (let index = startIndex; index < slashIndex; index += 1) {\n if (!isProductTokenChar(source.charCodeAt(index))) {\n return null;\n }\n }\n\n let endIndex = slashIndex + 1;\n while (endIndex < source.length && isProductTokenChar(source.charCodeAt(endIndex))) {\n endIndex += 1;\n }\n\n if (endIndex === slashIndex + 1) {\n return null;\n }\n\n return {\n name: source.slice(startIndex, slashIndex),\n version: source.slice(slashIndex + 1, endIndex),\n };\n};\n\nconst readVersionAfterProduct = (source: string, productName: string): string | null => {\n const token = readProductToken(source);\n return token?.name.toLowerCase() === productName.toLowerCase() ? token.version : null;\n};\n\nconst readVersionAfterKnownPrefix = (\n source: string,\n prefixes: readonly string[],\n): string | null => {\n const lowerSource = source.toLowerCase();\n let bestStart = -1;\n let bestPrefix = '';\n\n for (const prefix of prefixes) {\n const prefixStart = lowerSource.indexOf(prefix.toLowerCase());\n if (prefixStart !== -1 && (bestStart === -1 || prefixStart < bestStart)) {\n bestStart = prefixStart;\n bestPrefix = prefix;\n }\n }\n\n if (bestStart === -1) {\n return null;\n }\n\n const versionStart = bestStart + bestPrefix.length;\n let versionEnd = versionStart;\n while (versionEnd < source.length && isProductTokenChar(source.charCodeAt(versionEnd))) {\n versionEnd += 1;\n }\n\n return versionEnd > versionStart ? source.slice(versionStart, versionEnd) : null;\n};\n\nconst readVersionAfterKnownProduct = (\n source: string,\n productNames: readonly string[],\n): string | null =>\n readVersionAfterKnownPrefix(\n source,\n productNames.map((productName) => `${productName}/`),\n );\n\nconst readInternetExplorerVersion = (source: string): string | null => {\n const lowerSource = source.toLowerCase();\n const msieIndex = lowerSource.indexOf('msie');\n if (msieIndex !== -1) {\n let versionStart = msieIndex + 4;\n while (source[versionStart] === ' ') {\n versionStart += 1;\n }\n\n let versionEnd = versionStart;\n while (versionEnd < source.length) {\n const charCode = source.charCodeAt(versionEnd);\n if (!isDigit(charCode) && charCode !== 46) {\n break;\n }\n versionEnd += 1;\n }\n\n if (versionEnd > versionStart) {\n return source.slice(versionStart, versionEnd);\n }\n }\n\n const tridentIndex = lowerSource.indexOf('trident/');\n if (tridentIndex === -1) {\n return null;\n }\n\n const rvIndex = lowerSource.indexOf('rv:', tridentIndex + 8);\n if (rvIndex === -1) {\n return null;\n }\n\n const versionStart = rvIndex + 3;\n let versionEnd = versionStart;\n while (versionEnd < source.length) {\n const charCode = source.charCodeAt(versionEnd);\n if (!isDigit(charCode) && charCode !== 46) {\n break;\n }\n versionEnd += 1;\n }\n\n return versionEnd > versionStart ? source.slice(versionStart, versionEnd) : null;\n};\n\nconst readTrailingProductToken = (source: string, requireClosingParen: boolean): string | null => {\n let endIndex = source.length;\n while (endIndex > 0 && source.charCodeAt(endIndex - 1) === 32) {\n endIndex -= 1;\n }\n\n if (source[endIndex - 1] === ')') {\n endIndex -= 1;\n } else if (requireClosingParen) {\n return null;\n }\n\n let startIndex = endIndex;\n while (startIndex > 0 && isProductTokenChar(source.charCodeAt(startIndex - 1))) {\n startIndex -= 1;\n }\n\n return startIndex < endIndex ? source.slice(startIndex, endIndex) : null;\n};\n\nconst readIOSDeviceOSMatch = (source: string, device: 'iPad' | 'iPhone'): string | null => {\n const deviceLower = device.toLowerCase();\n let searchFrom = 0;\n\n while (searchFrom < source.length) {\n const openIndex = source.indexOf('(', searchFrom);\n if (openIndex === -1) {\n return null;\n }\n\n const closeIndex = source.indexOf(')', openIndex + 1);\n const segmentEnd = closeIndex === -1 ? source.length : closeIndex;\n const segment = source.slice(openIndex + 1, segmentEnd);\n const segmentLower = segment.toLowerCase();\n\n if (segmentLower.startsWith(deviceLower)) {\n let osSearchFrom = 0;\n while (osSearchFrom < segmentLower.length) {\n const osIndex = segmentLower.indexOf('os ', osSearchFrom);\n if (osIndex === -1) {\n break;\n }\n\n let versionStart = osIndex + 3;\n while (segment[versionStart] === ' ') {\n versionStart += 1;\n }\n\n let majorEnd = versionStart;\n while (majorEnd < segment.length && isDigit(segment.charCodeAt(majorEnd))) {\n majorEnd += 1;\n }\n\n const separator = segment[majorEnd];\n if (majorEnd > versionStart && (separator === '.' || separator === '_')) {\n let minorEnd = majorEnd + 1;\n while (minorEnd < segment.length && isDigit(segment.charCodeAt(minorEnd))) {\n minorEnd += 1;\n }\n\n if (minorEnd > majorEnd + 1) {\n return `(${segment.slice(0, minorEnd)}`.replace('_', '.');\n }\n }\n\n osSearchFrom = osIndex + 3;\n }\n }\n\n searchFrom = segmentEnd + 1;\n }\n\n return null;\n};\n\n/** WebKit DuckDuckGo pattern: \" Ddg/X.Y.Z\" at end of UA string */\nconst DUCKDUCKGO_WEBKIT_REGEXP = /\\sDdg\\/\\d+(?:\\.\\d+)*$/;\n\nexport class UserAgent {\n private readonly versions: Record<string, RegExp> = {\n Edge: /(?:edge|edga|edgios|edg)\\/([A-Za-z0-9_.-]+)/i,\n Firefox: /(?:firefox|fxios)\\/([A-Za-z0-9_.-]+)/i,\n IE: /msie\\s(\\d+(?:\\.\\d+)?)|trident\\/\\d+(?:\\.\\d+)?;[^)]*\\brv:(\\d+(?:\\.\\d+)?)/i,\n YaBrowser: /(?:yabrowser|yowser)\\/([A-Za-z0-9_.-]+)/i,\n Chrome: /(?:chrome|crios)\\/([A-Za-z0-9_.-]+)/i,\n Chromium: /chromium\\/([A-Za-z0-9_.-]+)/i,\n Safari: /(version|safari)\\/([A-Za-z0-9_.-]+)/i,\n Opera: /version\\/([A-Za-z0-9_.-]+)|OPR\\/([A-Za-z0-9_.-]+)/i,\n Amaya: /amaya\\/([A-Za-z0-9_.-]+)/i,\n SeaMonkey: /seamonkey\\/([A-Za-z0-9_.-]+)/i,\n OmniWeb: /omniweb\\/v([A-Za-z0-9_.-]+)/i,\n Flock: /flock\\/([A-Za-z0-9_.-]+)/i,\n Epiphany: /epiphany\\/([A-Za-z0-9_.-]+)/i,\n WinJs: /msapphost\\/([A-Za-z0-9_.-]+)/i,\n PhantomJS: /phantomjs\\/([A-Za-z0-9_.-]+)/i,\n AlamoFire: /alamofire\\/([A-Za-z0-9_.-]+)/i,\n UC: /ucbrowser\\/([A-Za-z0-9_.]+)/i,\n Facebook: /FBAV\\/([A-Za-z0-9_.]+)/i,\n WebKit: /applewebkit\\/([A-Za-z0-9_.]+)/i,\n Wechat: /micromessenger\\/([A-Za-z0-9_.]+)/i,\n Electron: /Electron\\/([A-Za-z0-9_.]+)/i,\n DuckDuckGo: /\\sDdg\\/(\\d+(?:\\.\\d+)*)$/i,\n };\n\n private readonly browsers: Record<string, RegExp> = {\n YaBrowser: /yabrowser|yowser/i,\n Edge: /edge|edga|edgios|edg/i,\n Amaya: /amaya/i,\n Konqueror: /konqueror/i,\n Epiphany: /epiphany/i,\n SeaMonkey: /seamonkey/i,\n Flock: /flock/i,\n OmniWeb: /omniweb/i,\n Chromium: /chromium/i,\n Chrome: /chrome|crios/i,\n Safari: /safari/i,\n IE: /msie|trident/i,\n Opera: /opera|OPR\\//i,\n PS3: /playstation 3/i,\n PSP: /playstation portable/i,\n Firefox: /firefox|fxios/i,\n WinJs: /msapphost/i,\n PhantomJS: /phantomjs/i,\n AlamoFire: /alamofire/i,\n UC: /UCBrowser/i,\n Facebook: /FBA[NV]/,\n DuckDuckGo: /\\sDdg\\/[\\d.]+$/i,\n };\n\n private readonly os: Record<string, RegExp> = {\n Windows11: /\\bwindows(?:\\s|_|)11(?:\\.\\d+)?/i,\n Windows10: /windows nt 10\\.0/i,\n Windows81: /windows nt 6\\.3/i,\n Windows8: /windows nt 6\\.2/i,\n Windows7: /windows nt 6\\.1/i,\n UnknownWindows: /windows nt 6\\.\\d+/i,\n WindowsVista: /windows nt 6\\.0/i,\n Windows2003: /windows nt 5\\.2/i,\n WindowsXP: /windows nt 5\\.1/i,\n Windows2000: /windows nt 5\\.0/i,\n WindowsPhone81: /windows phone 8\\.1/i,\n WindowsPhone80: /windows phone 8\\.0/i,\n OSXCheetah: /os x 10[._]0/i,\n OSXPuma: /os x 10[._]1(\\D|$)/i,\n OSXJaguar: /os x 10[._]2/i,\n OSXPanther: /os x 10[._]3/i,\n OSXTiger: /os x 10[._]4/i,\n OSXLeopard: /os x 10[._]5/i,\n OSXSnowLeopard: /os x 10[._]6/i,\n OSXLion: /os x 10[._]7/i,\n OSXMountainLion: /os x 10[._]8/i,\n OSXMavericks: /os x 10[._]9/i,\n OSXYosemite: /os x 10[._]10/i,\n OSXElCapitan: /os x 10[._]11/i,\n MacOSSierra: /os x 10[._]12/i,\n MacOSHighSierra: /os x 10[._]13/i,\n MacOSMojave: /os x 10[._]14/i,\n MacOSCatalina: /os x 10[._]15/i,\n MacOSBigSur: /(mac os x 10[._]16(?:[._]\\d+)?|mac os (?:x )?11[._]\\d+)/i,\n MacOSMonterey: /mac os (?:x )?12[._]\\d+/i,\n MacOSVentura: /mac os (?:x )?13[._]\\d+/i,\n MacOSSonoma: /mac os (?:x )?14[._]\\d+/i,\n MacOSSequoia: /mac os (?:x )?15[._]\\d+/i,\n MacOSTahoe: /mac os (?:x )?26[._]\\d+/i,\n Mac: /os x/i,\n Linux: /linux/i,\n Linux64: /linux x86_64/i,\n ChromeOS: /cros/i,\n Wii: /wii/i,\n PS3: /playstation 3/i,\n PSP: /playstation portable/i,\n iOS: /ios/i,\n Bada: /Bada\\/(\\d+)\\.(\\d+)/i,\n Curl: /curl\\/(\\d+)\\.(\\d+)\\.(\\d+)/i,\n Electron: /Electron\\/(\\d+)\\.(\\d+)\\.(\\d+)/i,\n };\n\n private readonly platform: Record<string, RegExp> = {\n Windows: /windows nt/i,\n WindowsPhone: /windows phone/i,\n Mac: /macintosh/i,\n Linux: /linux/i,\n Wii: /wii/i,\n Playstation: /playstation/i,\n iPad: /ipad/i,\n iPod: /ipod/i,\n iPhone: /iphone/i,\n Android: /android/i,\n Blackberry: /blackberry/i,\n Samsung: /samsung/i,\n Curl: /curl/i,\n Electron: /Electron/i,\n iOS: /^ios-/i,\n };\n\n public Agent: AgentDetails;\n\n constructor() {\n this.Agent = createDefaultAgent();\n }\n\n public reset(): this {\n this.Agent = createDefaultAgent();\n return this;\n }\n\n public testNginxGeoIP(headers: HeadersLike | IncomingHttpHeaders): this {\n Object.entries(headers ?? {}).forEach(([key, value]) => {\n if (/^GEOIP/i.test(key) && value !== undefined) {\n this.Agent.geoIp[key] = Array.isArray(value) ? value.join(',') : value;\n }\n });\n return this;\n }\n\n /** Maximum header length to process (prevents DoS from oversized headers) */\n private static readonly MAX_HEADER_LENGTH = 2048;\n /** Maximum number of brands to parse from a brand list */\n private static readonly MAX_BRAND_COUNT = 20;\n\n /**\n * Parse User-Agent Client Hints from HTTP headers\n * @see https://wicg.github.io/ua-client-hints/\n */\n public parseClientHints(headers: HeadersLike | IncomingHttpHeaders): ClientHints | null {\n const resolveHeader = (value: string | string[] | undefined): string => {\n try {\n if (value === null || value === undefined) {\n return '';\n }\n if (Array.isArray(value)) {\n const first = value[0];\n if (typeof first !== 'string') {\n return '';\n }\n return first.slice(0, UserAgent.MAX_HEADER_LENGTH);\n }\n if (typeof value !== 'string') {\n return '';\n }\n return value.slice(0, UserAgent.MAX_HEADER_LENGTH);\n } catch {\n return '';\n }\n };\n\n // Validate headers input\n if (headers === null || headers === undefined || typeof headers !== 'object') {\n return null;\n }\n\n // Normalize header keys to lowercase for case-insensitive lookup\n const normalizedHeaders: Record<string, string> = {};\n try {\n let headerCount = 0;\n const maxHeaders = 50; // Limit iterations over headers object\n for (const [key, value] of Object.entries(headers)) {\n if (++headerCount > maxHeaders) break;\n if (typeof key !== 'string') continue;\n normalizedHeaders[key.toLowerCase()] = resolveHeader(value);\n }\n } catch {\n return null;\n }\n\n const secChUa = normalizedHeaders['sec-ch-ua'];\n // Return null if no client hints are present\n if (!secChUa) {\n return null;\n }\n\n const parseBrandList = (header: string): ClientHintBrand[] => {\n try {\n if (!header || typeof header !== 'string') return [];\n const brands: ClientHintBrand[] = [];\n // Match patterns like: \"Brand\";v=\"version\" or \"Brand\"; v=\"version\"\n const brandRegex = /\"([^\"]{1,128})\";\\s*v=\"([^\"]{1,64})\"/g;\n let match;\n let iterations = 0;\n while ((match = brandRegex.exec(header)) !== null) {\n if (++iterations > UserAgent.MAX_BRAND_COUNT) break;\n brands.push({ brand: match[1], version: match[2] });\n }\n return brands;\n } catch {\n return [];\n }\n };\n\n const parseMobile = (header: string): boolean => {\n try {\n // ?1 = true, ?0 or empty = false\n if (typeof header !== 'string') return false;\n return header === '?1';\n } catch {\n return false;\n }\n };\n\n const parseQuotedString = (header: string): string => {\n try {\n if (typeof header !== 'string') return '';\n // Limit input length before regex\n const truncated = header.slice(0, 256);\n // Remove surrounding quotes if present\n const match = /^\"([^\"]*)\"$/.exec(truncated);\n return match ? match[1] : truncated;\n } catch {\n return '';\n }\n };\n\n try {\n const clientHints: ClientHints = {\n brands: parseBrandList(secChUa),\n mobile: parseMobile(normalizedHeaders['sec-ch-ua-mobile'] ?? ''),\n platform: parseQuotedString(normalizedHeaders['sec-ch-ua-platform'] ?? ''),\n platformVersion: parseQuotedString(normalizedHeaders['sec-ch-ua-platform-version'] ?? ''),\n architecture: parseQuotedString(normalizedHeaders['sec-ch-ua-arch'] ?? ''),\n bitness: parseQuotedString(normalizedHeaders['sec-ch-ua-bitness'] ?? ''),\n model: parseQuotedString(normalizedHeaders['sec-ch-ua-model'] ?? ''),\n fullVersionList: parseBrandList(normalizedHeaders['sec-ch-ua-full-version-list'] ?? ''),\n };\n\n this.Agent.clientHints = clientHints;\n return clientHints;\n } catch {\n return null;\n }\n }\n\n /**\n * Test for DuckDuckGo browser using both Client Hints and UA string patterns\n * - Chromium platforms (Android, Windows): Sec-CH-UA brand \"DuckDuckGo\"\n * - WebKit platforms (iOS, macOS): UA string ends with \" Ddg/X.Y.Z\"\n */\n public testDuckDuckGo(): void {\n // Check client hints brands first (Chromium-based DDG)\n if (this.Agent.clientHints?.brands) {\n const hasDdgBrand = this.Agent.clientHints.brands.some(\n (brand) => brand.brand === 'DuckDuckGo',\n );\n if (hasDdgBrand) {\n this.Agent.isDuckDuckGo = true;\n this.Agent.browser = 'DuckDuckGo';\n this.Agent.version = this.getDuckDuckGoVersion() ?? 'unknown';\n return;\n }\n }\n\n // Check full version list as well\n if (this.Agent.clientHints?.fullVersionList) {\n const hasDdgBrand = this.Agent.clientHints.fullVersionList.some(\n (brand) => brand.brand === 'DuckDuckGo',\n );\n if (hasDdgBrand) {\n this.Agent.isDuckDuckGo = true;\n this.Agent.browser = 'DuckDuckGo';\n this.Agent.version = this.getDuckDuckGoVersion() ?? 'unknown';\n return;\n }\n }\n\n // Fallback: check WebKit UA string pattern (iOS/macOS DDG)\n if (DUCKDUCKGO_WEBKIT_REGEXP.test(this.Agent.source)) {\n this.Agent.isDuckDuckGo = true;\n this.Agent.browser = 'DuckDuckGo';\n this.Agent.version = this.getDuckDuckGoVersion() ?? 'unknown';\n }\n }\n\n public testBot(): void {\n const source = this.Agent.source.toLowerCase();\n const match = IS_BOT_REGEXP.exec(source);\n\n if (match) {\n const botIdentifier = match[1];\n\n // Handle false positives - TikTok WebView contains \"googleplay\" but isn't a bot\n if (\n botIdentifier === 'google' &&\n (source.includes('tiktok') || source.includes('trill') || source.includes('bytedance'))\n ) {\n this.Agent.isBot = false;\n this.Agent.botName = '';\n return;\n }\n\n // For all bots, return boolean true and store bot name (fixes issues #168, #138)\n this.Agent.isBot = true;\n this.Agent.botName = botIdentifier;\n } else if (!this.Agent.isAuthoritative) {\n this.Agent.isBot = /bot/i.test(this.Agent.source);\n this.Agent.botName = this.Agent.isBot ? 'bot' : '';\n } else {\n this.Agent.isBot = false;\n this.Agent.botName = '';\n }\n }\n\n public testSmartTV(): void {\n this.Agent.isSmartTV = SMART_TV_REGEXP.test(this.Agent.source.toLowerCase());\n }\n\n public testMobile(): void {\n if (this.Agent.isWindows || this.Agent.isLinux || this.Agent.isMac || this.Agent.isChromeOS) {\n this.Agent.isDesktop = true;\n } else if (this.Agent.isAndroid || this.Agent.isSamsung) {\n this.Agent.isMobile = true;\n }\n\n if (\n this.Agent.isiPad ||\n this.Agent.isiPod ||\n this.Agent.isiPhone ||\n this.Agent.isBada ||\n this.Agent.isBlackberry ||\n this.Agent.isAndroid ||\n this.Agent.isWindowsPhone\n ) {\n this.Agent.isMobile = true;\n this.Agent.isDesktop = false;\n }\n\n if (MOBILE_REGEXP.test(this.Agent.source)) {\n this.Agent.isMobile = true;\n this.Agent.isDesktop = false;\n }\n\n if (DALVIK_REGEXP.test(this.Agent.source)) {\n this.Agent.isAndroidNative = true;\n this.Agent.isMobileNative = true;\n }\n\n if (OKHTTP_REGEXP.test(this.Agent.source)) {\n this.Agent.isAndroidNative = true;\n this.Agent.isMobileNative = true;\n this.Agent.isMobile = true;\n this.Agent.isAndroid = true;\n this.Agent.isDesktop = false;\n }\n\n if (IOS_SCALE_REGEXP.test(this.Agent.source)) {\n this.Agent.isiPhoneNative = true;\n this.Agent.isMobileNative = true;\n }\n }\n\n public testAndroidTablet(): void {\n if (this.Agent.isAndroid && !ANDROID_TABLET_REGEXP.test(this.Agent.source)) {\n this.Agent.isAndroidTablet = true;\n }\n }\n\n public testTablet(): void {\n if (this.Agent.isiPad || this.Agent.isAndroidTablet || this.Agent.isKindleFire) {\n this.Agent.isTablet = true;\n }\n\n if (/tablet/i.test(this.Agent.source)) {\n this.Agent.isTablet = true;\n }\n }\n\n public testCompatibilityMode(): void {\n if (!this.Agent.isIE) {\n return;\n }\n\n const tridentMatch = /Trident\\/(\\d)\\.0/i.exec(this.Agent.source);\n if (!tridentMatch) {\n return;\n }\n\n const tridentVersion = parseInt(tridentMatch[1], 10);\n const version = parseInt(String(this.Agent.version), 10);\n\n if (version === 7 && tridentVersion === 7) {\n this.Agent.isIECompatibilityMode = true;\n this.Agent.version = '11.0';\n }\n if (version === 7 && tridentVersion === 6) {\n this.Agent.isIECompatibilityMode = true;\n this.Agent.version = '10.0';\n }\n if (version === 7 && tridentVersion === 5) {\n this.Agent.isIECompatibilityMode = true;\n this.Agent.version = '9.0';\n }\n if (version === 7 && tridentVersion === 4) {\n this.Agent.isIECompatibilityMode = true;\n this.Agent.version = '8.0';\n }\n }\n\n public testSilk(): string | false {\n if (SILK_REGEXP.test(this.Agent.source)) {\n this.Agent.isSilk = true;\n }\n\n if (SILK_ACCELERATED_REGEXP.test(this.Agent.source)) {\n this.Agent.silkAccelerated = true;\n this.Agent.SilkAccelerated = true;\n }\n\n return this.Agent.isSilk ? 'Silk' : false;\n }\n\n public testKindleFire(): string | false {\n const { source } = this.Agent;\n if (/KFOT/gi.test(source)) {\n this.Agent.isKindleFire = true;\n return 'Kindle Fire';\n }\n if (/KFTT/gi.test(source)) {\n this.Agent.isKindleFire = true;\n return 'Kindle Fire HD';\n }\n if (/KFJWI/gi.test(source)) {\n this.Agent.isKindleFire = true;\n return 'Kindle Fire HD 8.9';\n }\n if (/KFJWA/gi.test(source)) {\n this.Agent.isKindleFire = true;\n return 'Kindle Fire HD 8.9 4G';\n }\n if (/KFSOWI/gi.test(source)) {\n this.Agent.isKindleFire = true;\n return 'Kindle Fire HD 7';\n }\n if (/KFTHWI/gi.test(source)) {\n this.Agent.isKindleFire = true;\n return 'Kindle Fire HDX 7';\n }\n if (/KFTHWA/gi.test(source)) {\n this.Agent.isKindleFire = true;\n return 'Kindle Fire HDX 7 4G';\n }\n if (/KFAPWI/gi.test(source)) {\n this.Agent.isKindleFire = true;\n return 'Kindle Fire HDX 8.9';\n }\n if (/KFAPWA/gi.test(source)) {\n this.Agent.isKindleFire = true;\n return 'Kindle Fire HDX 8.9 4G';\n }\n return false;\n }\n\n public testCaptiveNetwork(): string | false {\n if (/CaptiveNetwork/gi.test(this.Agent.source)) {\n this.Agent.isCaptive = true;\n this.Agent.isMac = true;\n this.Agent.platform = 'Apple Mac';\n return 'CaptiveNetwork';\n }\n return false;\n }\n\n public testWebkit(): void {\n if (this.Agent.browser === 'unknown' && WEBKIT_REGEXP.test(this.Agent.source)) {\n this.Agent.browser = 'Apple WebKit';\n this.Agent.isWebkit = true;\n }\n }\n\n public testWechat(): void {\n if (WECHAT_REGEXP.test(this.Agent.source)) {\n this.Agent.isWechat = true;\n this.Agent.version = this.getWechatVersion(this.Agent.source);\n }\n }\n\n public parse(source: string): AgentDetails {\n return new UserAgent().hydrate(source).Agent;\n }\n\n /**\n * Hydrate agent from UA string and HTTP headers (including Client Hints)\n * This method should be preferred when headers are available as it enables\n * detection of browsers that use Client Hints (e.g., DuckDuckGo on Chromium)\n */\n public hydrateFromHeaders(source: string, headers: HeadersLike | IncomingHttpHeaders): this {\n this.hydrate(source);\n this.parseClientHints(headers);\n this.testDuckDuckGo();\n return this;\n }\n\n public hydrate(source: string): this {\n this.Agent = createDefaultAgent();\n this.Agent.source = source.trim();\n this.Agent.os = this.getOS(this.Agent.source);\n this.Agent.platform = this.getPlatform(this.Agent.source);\n this.Agent.browser = this.getBrowser(this.Agent.source);\n this.Agent.version = this.getBrowserVersion(this.Agent.source);\n this.Agent.electronVersion = this.getElectronVersion(this.Agent.source);\n this.testBot();\n this.testSmartTV();\n this.testMobile();\n this.testAndroidTablet();\n this.testTablet();\n this.testCompatibilityMode();\n this.testSilk();\n this.testKindleFire();\n this.testCaptiveNetwork();\n this.testWebkit();\n this.testWechat();\n return this;\n }\n\n private getBrowser(string: string): string {\n const agent = this.Agent;\n if (this.browsers.YaBrowser.test(string)) {\n agent.isYaBrowser = true;\n return 'YaBrowser';\n }\n if (this.browsers.AlamoFire.test(string)) {\n agent.isAlamoFire = true;\n return 'AlamoFire';\n }\n if (this.browsers.Edge.test(string)) {\n agent.isEdge = true;\n return 'Edge';\n }\n if (this.browsers.PhantomJS.test(string)) {\n agent.isPhantomJS = true;\n return 'PhantomJS';\n }\n if (this.browsers.Konqueror.test(string)) {\n agent.isKonqueror = true;\n return 'Konqueror';\n }\n if (this.browsers.Amaya.test(string)) {\n agent.isAmaya = true;\n return 'Amaya';\n }\n if (this.browsers.Epiphany.test(string)) {\n agent.isEpiphany = true;\n return 'Epiphany';\n }\n if (this.browsers.SeaMonkey.test(string)) {\n agent.isSeaMonkey = true;\n return 'SeaMonkey';\n }\n if (this.browsers.Flock.test(string)) {\n agent.isFlock = true;\n return 'Flock';\n }\n if (this.browsers.OmniWeb.test(string)) {\n agent.isOmniWeb = true;\n return 'OmniWeb';\n }\n if (this.browsers.Opera.test(string)) {\n agent.isOpera = true;\n return 'Opera';\n }\n if (this.browsers.Chromium.test(string)) {\n agent.isChrome = true;\n return 'Chromium';\n }\n if (this.browsers.Facebook.test(string)) {\n agent.isFacebook = true;\n return 'Facebook';\n }\n if (this.browsers.Chrome.test(string)) {\n agent.isChrome = true;\n return 'Chrome';\n }\n if (this.browsers.WinJs.test(string)) {\n agent.isWinJs = true;\n return 'WinJs';\n }\n if (this.browsers.IE.test(string)) {\n agent.isIE = true;\n return 'IE';\n }\n if (this.browsers.Firefox.test(string)) {\n agent.isFirefox = true;\n return 'Firefox';\n }\n // Android Browser: stock AOSP browser - has Android + Version/ + Mobile Safari/ but no Chrome or other browser tokens (Bug #80)\n if (\n /android/i.test(string) &&\n /version\\//i.test(string) &&\n /mobile safari\\//i.test(string) &&\n !/chrome/i.test(string) &&\n !/silk/i.test(string)\n ) {\n agent.isAndroid = true;\n return 'Android Browser';\n }\n if (this.browsers.Safari.test(string)) {\n agent.isSafari = true;\n return 'Safari';\n }\n if (this.browsers.PS3.test(string)) {\n return 'ps3';\n }\n if (this.browsers.PSP.test(string)) {\n return 'psp';\n }\n if (this.browsers.UC.test(string)) {\n agent.isUC = true;\n return 'UCBrowser';\n }\n\n if (string.includes('Dalvik')) {\n return 'unknown';\n }\n\n if (!string.startsWith('Mozilla')) {\n const guess = readProductToken(string);\n if (guess) {\n agent.isAuthoritative = false;\n return guess.name;\n }\n }\n\n return 'unknown';\n }\n\n private getBrowserVersion(string: string): string {\n const agent = this.Agent;\n const browser = agent.browser;\n\n switch (browser) {\n case 'Edge':\n return readVersionAfterKnownProduct(string, ['edge', 'edga', 'edgios', 'edg']) ?? 'unknown';\n case 'PhantomJS':\n return readVersionAfterKnownProduct(string, ['phantomjs']) ?? 'unknown';\n case 'YaBrowser':\n return readVersionAfterKnownProduct(string, ['yabrowser', 'yowser']) ?? 'unknown';\n case 'Chrome':\n return readVersionAfterKnownProduct(string, ['chrome', 'crios']) ?? 'unknown';\n case 'Chromium':\n return readVersionAfterKnownProduct(string, ['chromium']) ?? 'unknown';\n case 'Safari':\n return readVersionAfterKnownProduct(string, ['version', 'safari']) ?? 'unknown';\n case 'Opera':\n return readVersionAfterKnownProduct(string, ['version', 'OPR']) ?? 'unknown';\n case 'Firefox':\n return readVersionAfterKnownProduct(string, ['firefox', 'fxios']) ?? 'unknown';\n case 'WinJs':\n return readVersionAfterKnownProduct(string, ['msapphost']) ?? 'unknown';\n case 'IE':\n return readInternetExplorerVersion(string) ?? 'unknown';\n case 'ps3':\n return readTrailingProductToken(string, true) ?? 'unknown';\n case 'psp':\n return readTrailingProductToken(string, false) ?? 'unknown';\n case 'Amaya':\n return readVersionAfterKnownProduct(string, ['amaya']) ?? 'unknown';\n case 'Epiphany':\n return readVersionAfterKnownProduct(string, ['epiphany']) ?? 'unknown';\n case 'SeaMonkey':\n return readVersionAfterKnownProduct(string, ['seamonkey']) ?? 'unknown';\n case 'Flock':\n return readVersionAfterKnownProduct(string, ['flock']) ?? 'unknown';\n case 'OmniWeb':\n return readVersionAfterKnownPrefix(string, ['omniweb/v']) ?? 'unknown';\n case 'UCBrowser':\n return readVersionAfterKnownProduct(string, ['ucbrowser']) ?? 'unknown';\n case 'Facebook':\n return readVersionAfterKnownProduct(string, ['FBAV']) ?? 'unknown';\n case 'Android Browser':\n // Android Browser reports version via Version/X.X token (Bug #80)\n return readVersionAfterKnownProduct(string, ['version', 'safari']) ?? 'unknown';\n case 'DuckDuckGo':\n return this.getDuckDuckGoVersion() ?? 'unknown';\n default:\n if (browser !== 'unknown') {\n return readVersionAfterProduct(string, browser) ?? 'unknown';\n } else {\n this.testWebkit();\n if (this.Agent.isWebkit) {\n return readVersionAfterKnownProduct(string, ['applewebkit']) ?? 'unknown';\n }\n }\n }\n\n return 'unknown';\n }\n\n private getWechatVersion(string: string): string {\n const match = string.match(this.versions.Wechat);\n return match ? match[1] : 'unknown';\n }\n\n private getDuckDuckGoVersion(): string | null {\n // Try client hints first\n const hints = this.Agent.clientHints;\n if (hints) {\n // Check fullVersionList first for more precise version\n const fullBrand = hints.fullVersionList.find((b) => b.brand === 'DuckDuckGo');\n if (fullBrand) {\n return fullBrand.version;\n }\n // Fall back to brands\n const brand = hints.brands.find((b) => b.brand === 'DuckDuckGo');\n if (brand) {\n return brand.version;\n }\n }\n // Fall back to UA string pattern\n const match = this.Agent.source.match(this.versions.DuckDuckGo);\n return match ? match[1] : null;\n }\n\n private getElectronVersion(string: string): string {\n const match = string.match(this.versions.Electron);\n if (match) {\n this.Agent.isElectron = true;\n return match[1];\n }\n return '';\n }\n\n private getOS(string: string): string {\n if (this.os.WindowsVista.test(string)) {\n this.Agent.isWindows = true;\n return 'Windows Vista';\n }\n if (this.os.Windows7.test(string)) {\n this.Agent.isWindows = true;\n return 'Windows 7';\n }\n if (this.os.Windows8.test(string)) {\n this.Agent.isWindows = true;\n return 'Windows 8';\n }\n if (this.os.Windows81.test(string)) {\n this.Agent.isWindows = true;\n return 'Windows 8.1';\n }\n if (this.os.Windows11.test(string)) {\n this.Agent.isWindows = true;\n return 'Windows 11';\n }\n if (this.os.Windows10.test(string)) {\n this.Agent.isWindows = true;\n return 'Windows 10.0';\n }\n if (this.os.Windows2003.test(string)) {\n this.Agent.isWindows = true;\n return 'Windows 2003';\n }\n if (this.os.WindowsXP.test(string)) {\n this.Agent.isWindows = true;\n return 'Windows XP';\n }\n if (this.os.Windows2000.test(string)) {\n this.Agent.isWindows = true;\n return 'Windows 2000';\n }\n if (this.os.WindowsPhone81.test(string)) {\n this.Agent.isWindowsPhone = true;\n return 'Windows Phone 8.1';\n }\n if (this.os.WindowsPhone80.test(string)) {\n this.Agent.isWindowsPhone = true;\n return 'Windows Phone 8.0';\n }\n if (this.os.Linux64.test(string)) {\n this.Agent.isLinux = true;\n this.Agent.isLinux64 = true;\n return 'Linux 64';\n }\n if (this.os.Linux.test(string)) {\n this.Agent.isLinux = true;\n return 'Linux';\n }\n if (this.os.ChromeOS.test(string)) {\n this.Agent.isChromeOS = true;\n return 'Chrome OS';\n }\n if (this.os.Wii.test(string)) {\n return 'Wii';\n }\n if (this.os.PS3.test(string)) {\n return 'Playstation';\n }\n if (this.os.PSP.test(string)) {\n return 'Playstation';\n }\n if (this.os.OSXCheetah.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Cheetah';\n }\n if (this.os.OSXPuma.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Puma';\n }\n if (this.os.OSXJaguar.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Jaguar';\n }\n if (this.os.OSXPanther.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Panther';\n }\n if (this.os.OSXTiger.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Tiger';\n }\n if (this.os.OSXLeopard.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Leopard';\n }\n if (this.os.OSXSnowLeopard.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Snow Leopard';\n }\n if (this.os.OSXLion.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Lion';\n }\n if (this.os.OSXMountainLion.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Mountain Lion';\n }\n if (this.os.OSXMavericks.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Mavericks';\n }\n if (this.os.OSXYosemite.test(string)) {\n this.Agent.isMac = true;\n return 'OS X Yosemite';\n }\n if (this.os.OSXElCapitan.test(string)) {\n this.Agent.isMac = true;\n return 'OS X El Capitan';\n }\n if (this.os.MacOSSierra.test(string)) {\n this.Agent.isMac = true;\n return 'macOS Sierra';\n }\n if (this.os.MacOSHighSierra.test(string)) {\n this.Agent.isMac = true;\n return 'macOS High Sierra';\n }\n if (this.os.MacOSMojave.test(string)) {\n this.Agent.isMac = true;\n return 'macOS Mojave';\n }\n if (this.os.MacOSCatalina.test(string)) {\n this.Agent.isMac = true;\n return 'macOS Catalina';\n }\n if (this.os.MacOSBigSur.test(string)) {\n this.Agent.isMac = true;\n return 'macOS Big Sur';\n }\n if (this.os.MacOSMonterey.test(string)) {\n this.Agent.isMac = true;\n return 'macOS Monterey';\n }\n if (this.os.MacOSVentura.test(string)) {\n this.Agent.isMac = true;\n return 'macOS Ventura';\n }\n if (this.os.MacOSSonoma.test(string)) {\n this.Agent.isMac = true;\n return 'macOS Sonoma';\n }\n if (this.os.MacOSSequoia.test(string)) {\n this.Agent.isMac = true;\n return 'macOS Sequoia';\n }\n if (this.os.MacOSTahoe.test(string)) {\n this.Agent.isMac = true;\n return 'macOS Tahoe';\n }\n if (this.os.Mac.test(string)) {\n this.Agent.isMac = true;\n return 'OS X';\n }\n const iPadMatch = readIOSDeviceOSMatch(string, 'iPad');\n if (iPadMatch) {\n this.Agent.isiPad = true;\n return iPadMatch;\n }\n const iPhoneMatch = readIOSDeviceOSMatch(string, 'iPhone');\n if (iPhoneMatch) {\n this.Agent.isiPhone = true;\n return iPhoneMatch;\n }\n if (this.os.Bada.test(string)) {\n this.Agent.isBada = true;\n return 'Bada';\n }\n if (this.os.Curl.test(string)) {\n this.Agent.isCurl = true;\n return 'Curl';\n }\n if (this.os.iOS.test(string)) {\n this.Agent.isiPhone = true;\n return 'iOS';\n }\n if (this.os.Electron.test(string)) {\n this.Agent.isElectron = true;\n return 'Electron';\n }\n return 'unknown';\n }\n\n private getPlatform(string: string): string {\n if (this.platform.Windows.test(string)) {\n return 'Microsoft Windows';\n }\n if (this.platform.WindowsPhone.test(string)) {\n this.Agent.isWindowsPhone = true;\n return 'Microsoft Windows Phone';\n }\n if (this.platform.Mac.test(string)) {\n return 'Apple Mac';\n }\n if (this.platform.Curl.test(string)) {\n return 'Curl';\n }\n if (this.platform.Electron.test(string)) {\n this.Agent.isElectron = true;\n return 'Electron';\n }\n if (this.platform.Android.test(string)) {\n this.Agent.isAndroid = true;\n // Also detect Samsung devices within Android platform (Bug #104)\n if (this.platform.Samsung.test(string)) {\n this.Agent.isSamsung = true;\n }\n return 'Android';\n }\n if (this.platform.Blackberry.test(string)) {\n this.Agent.isBlackberry = true;\n return 'Blackberry';\n }\n if (this.platform.Linux.test(string)) {\n return 'Linux';\n }\n if (this.platform.Wii.test(string)) {\n return 'Wii';\n }\n if (this.platform.Playstation.test(string)) {\n return 'Playstation';\n }\n if (this.platform.iPad.test(string)) {\n this.Agent.isiPad = true;\n return 'iPad';\n }\n if (this.platform.iPod.test(string)) {\n this.Agent.isiPod = true;\n return 'iPod';\n }\n if (this.platform.iPhone.test(string)) {\n this.Agent.isiPhone = true;\n return 'iPhone';\n }\n if (this.platform.Samsung.test(string)) {\n this.Agent.isSamsung = true;\n return 'Samsung';\n }\n if (this.platform.iOS.test(string)) {\n return 'Apple iOS';\n }\n return 'unknown';\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC0BA,IAAM,OAAO;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,gBAAgB,IAAI,OAAO,IAAI,KAAK,KAAK,GAAG,CAAC,KAAK,GAAG;AAC3D,IAAM,cAAc;AACpB,IAAM,0BAA0B;AAChC,IAAM,kBAAkB;AACxB,IAAM,wBAAwB;AAC9B,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AACtB,IAAM,mBAAmB;AACzB,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AAuEtB,IAAM,gBAA8B;AAAA,EAClC,aAAa;AAAA,EACb,iBAAiB;AAAA,EACjB,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,uBAAuB;AAAA,EACvB,UAAU;AAAA,EACV,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,aAAa;AAAA,EACb,WAAW;AAAA,EACX,aAAa;AAAA,EACb,SAAS;AAAA,EACT,SAAS;AAAA,EACT,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AAAA,EACT,WAAW;AAAA,EACX,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,aAAa;AAAA,EACb,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,SAAS;AAAA,EACT,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,WAAW;AAAA,EACX,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,OAAO,CAAC;AAAA,EACR,QAAQ;AAAA,EACR,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,aAAa;AACf;AAEA,SAAS,qBAAmC;AAC1C,SAAO;AAAA,IACL,GAAG;AAAA,IACH,OAAO,CAAC;AAAA,IACR,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB,SAAS;AAAA,IACT,aAAa;AAAA,EACf;AACF;AAOA,IAAM,qBAAqB,CAAC,aACzB,YAAY,MAAM,YAAY,MAC9B,YAAY,MAAM,YAAY,MAC9B,YAAY,MAAM,YAAY,OAC/B,aAAa,MACb,aAAa,MACb,aAAa;AAEf,IAAM,UAAU,CAAC,aAA8B,YAAY,MAAM,YAAY;AAE7E,IAAM,mBAAmB,CAAC,QAAgB,aAAa,MAA2B;AAChF,QAAM,aAAa,OAAO,QAAQ,KAAK,UAAU;AACjD,MAAI,cAAc,YAAY;AAC5B,WAAO;AAAA,EACT;AAEA,WAAS,QAAQ,YAAY,QAAQ,YAAY,SAAS,GAAG;AAC3D,QAAI,CAAC,mBAAmB,OAAO,WAAW,KAAK,CAAC,GAAG;AACjD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,WAAW,aAAa;AAC5B,SAAO,WAAW,OAAO,UAAU,mBAAmB,OAAO,WAAW,QAAQ,CAAC,GAAG;AAClF,gBAAY;AAAA,EACd;AAEA,MAAI,aAAa,aAAa,GAAG;AAC/B,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,OAAO,MAAM,YAAY,UAAU;AAAA,IACzC,SAAS,OAAO,MAAM,aAAa,GAAG,QAAQ;AAAA,EAChD;AACF;AAEA,IAAM,0BAA0B,CAAC,QAAgB,gBAAuC;AACtF,QAAM,QAAQ,iBAAiB,MAAM;AACrC,SAAO,OAAO,KAAK,YAAY,MAAM,YAAY,YAAY,IAAI,MAAM,UAAU;AACnF;AAEA,IAAM,8BAA8B,CAClC,QACA,aACkB;AAClB,QAAM,cAAc,OAAO,YAAY;AACvC,MAAI,YAAY;AAChB,MAAI,aAAa;AAEjB,aAAW,UAAU,UAAU;AAC7B,UAAM,cAAc,YAAY,QAAQ,OAAO,YAAY,CAAC;AAC5D,QAAI,gBAAgB,OAAO,cAAc,MAAM,cAAc,YAAY;AACvE,kBAAY;AACZ,mBAAa;AAAA,IACf;AAAA,EACF;AAEA,MAAI,cAAc,IAAI;AACpB,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,YAAY,WAAW;AAC5C,MAAI,aAAa;AACjB,SAAO,aAAa,OAAO,UAAU,mBAAmB,OAAO,WAAW,UAAU,CAAC,GAAG;AACtF,kBAAc;AAAA,EAChB;AAEA,SAAO,aAAa,eAAe,OAAO,MAAM,cAAc,UAAU,IAAI;AAC9E;AAEA,IAAM,+BAA+B,CACnC,QACA,iBAEA;AAAA,EACE;AAAA,EACA,aAAa,IAAI,CAAC,gBAAgB,GAAG,WAAW,GAAG;AACrD;AAEF,IAAM,8BAA8B,CAAC,WAAkC;AACrE,QAAM,cAAc,OAAO,YAAY;AACvC,QAAM,YAAY,YAAY,QAAQ,MAAM;AAC5C,MAAI,cAAc,IAAI;AACpB,QAAIA,gBAAe,YAAY;AAC/B,WAAO,OAAOA,aAAY,MAAM,KAAK;AACnC,MAAAA,iBAAgB;AAAA,IAClB;AAEA,QAAIC,cAAaD;AACjB,WAAOC,cAAa,OAAO,QAAQ;AACjC,YAAM,WAAW,OAAO,WAAWA,WAAU;AAC7C,UAAI,CAAC,QAAQ,QAAQ,KAAK,aAAa,IAAI;AACzC;AAAA,MACF;AACA,MAAAA,eAAc;AAAA,IAChB;AAEA,QAAIA,cAAaD,eAAc;AAC7B,aAAO,OAAO,MAAMA,eAAcC,WAAU;AAAA,IAC9C;AAAA,EACF;AAEA,QAAM,eAAe,YAAY,QAAQ,UAAU;AACnD,MAAI,iBAAiB,IAAI;AACvB,WAAO;AAAA,EACT;AAEA,