ua-browser 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -22,6 +22,23 @@ Detect browser, OS, device type, rendering engine, CPU architecture, bots, headl
22
22
  - **TypeScript** — full type definitions with precise literal union types (`BrowserName`, `OsName`, etc.)
23
23
  - **Tree-shakeable** — named exports + `sideEffects: false`, unused code eliminated by Vite / Rollup / webpack 5+
24
24
 
25
+ ## Why ua-browser
26
+
27
+ UA strings lie — a phone in desktop mode, a headless browser, or an AI crawler can all masquerade as an ordinary user. ua-browser combines hardware signals and Client Hints to stay accurate when the UA string can't be trusted.
28
+
29
+ | Capability | ua-browser | ua-parser-js | bowser | detect-browser |
30
+ | :-- | :--: | :--: | :--: | :--: |
31
+ | UA string parsing | ✅ | ✅ | ✅ | ✅ |
32
+ | Zero dependencies | ✅ | ✅ | ✅ | ✅ |
33
+ | TypeScript native | ✅ | ✅ | ✅ | ✅ |
34
+ | Tree-shakeable | ✅ | ❌ | ✅ | ❌ |
35
+ | Hardware-signal device detection (accurate in desktop mode) | ✅ | ❌ | ❌ | ❌ |
36
+ | CPU architecture (Apple Silicon vs Intel) | ✅ | ❌ | ❌ | ❌ |
37
+ | SSR Client Hints | ✅ | ❌ | ❌ | ❌ |
38
+ | Headless browser detection | ✅ | ❌ | ❌ | ❌ |
39
+ | AI bot recognition (40+ rules) | ✅ | ❌ | ❌ | ❌ |
40
+ | Extended device types (TV / Console / XR) | ✅ | ❌ | ❌ | ❌ |
41
+
25
42
  ## Installation
26
43
 
27
44
  ```sh
@@ -79,6 +96,8 @@ if (result.device === 'Mobile') {
79
96
  }
80
97
  ```
81
98
 
99
+ > **Note**: `detect()` uses the Client Hints high-entropy API (`getHighEntropyValues`), which is only available in **HTTPS or localhost** contexts. On plain HTTP pages it degrades silently — browser version and OS version fall back to the frozen UA string values (e.g. Chrome reports `149.0.0.0`, macOS 26+ reports `10.15.7`).
100
+
82
101
  ### Browser (sync: `uaBrowser`)
83
102
 
84
103
  ```typescript
@@ -223,6 +242,28 @@ Highlights:
223
242
  - **Bots** — GPTBot, ClaudeBot, PerplexityBot, CCBot; messaging bots (Slack, Discord, Telegram, WhatsApp) and more
224
243
  - **Devices** — Mobile, Tablet, PC, TV (Samsung Smart TV, HbbTV), Console (PS5, Xbox, Switch), XR (Vision Pro, Quest)
225
244
 
245
+ ## FAQ
246
+
247
+ **How is ua-browser different from ua-parser-js?**
248
+
249
+ `ua-parser-js` focuses on parsing the UA string itself and has no hardware-signal collection. It misidentifies device type when a phone is in desktop mode or when the UA is spoofed. ua-browser adds WebGL renderer, Client Hints, CSS `safe-area-inset`, and sensor APIs to detect the actual hardware — plus 40+ AI bot rules and headless browser detection that `ua-parser-js` does not include.
250
+
251
+ **Does it work in Next.js / Nuxt / other SSR frameworks?**
252
+
253
+ Yes. `parseUA(ua)` is a pure function with no browser API dependencies — it runs in Node.js, Deno, and Edge Runtime as-is. Pair `parseHeaders()` with `ACCEPT_CH` to leverage Client Hints for precise architecture and platform data on the server.
254
+
255
+ **Can it detect mobile devices when the user has enabled desktop mode?**
256
+
257
+ Yes, when you use `uaBrowser.detect()` or `getEnvContext()`. These APIs collect CSS `safe-area-inset`, the Vibration API, and device pixel ratio to identify the actual hardware, independent of what the UA string declares.
258
+
259
+ **How do I detect GPT, Claude, or other AI crawler requests?**
260
+
261
+ Check the `isBot` and `botName` fields on the return value. Built-in rules cover GPTBot, ClaudeBot, PerplexityBot, CCBot, and messaging link-preview bots (Slack, Discord, Telegram, WhatsApp).
262
+
263
+ **What is the bundle size?**
264
+
265
+ Zero runtime dependencies. The bundle is tiny after gzip; tree-shaking named exports makes it smaller still.
266
+
226
267
  ## License
227
268
 
228
269
  [MIT](./LICENSE) © yangtianxia
package/README.md CHANGED
@@ -21,6 +21,23 @@
21
21
  - **TypeScript** — 完整类型定义,`BrowserName`、`OsName` 等均为精确字面量联合类型
22
22
  - **Tree-shakeable** — 所有功能按需导入,不引入多余代码
23
23
 
24
+ ## 为什么选 ua-browser
25
+
26
+ UA 字符串会撒谎 —— 开了桌面模式的手机、无头浏览器、AI 爬虫都可能伪装成普通用户。ua-browser 额外引入硬件信号与 Client Hints,在 UA 失真时依然准确。
27
+
28
+ | 能力 | ua-browser | ua-parser-js | bowser | detect-browser |
29
+ | :-- | :--: | :--: | :--: | :--: |
30
+ | UA 字符串解析 | ✅ | ✅ | ✅ | ✅ |
31
+ | 零依赖 | ✅ | ✅ | ✅ | ✅ |
32
+ | TypeScript 原生 | ✅ | ✅ | ✅ | ✅ |
33
+ | Tree-shakeable | ✅ | ❌ | ✅ | ❌ |
34
+ | 硬件信号设备检测(桌面模式下仍准确)| ✅ | ❌ | ❌ | ❌ |
35
+ | CPU 架构(Apple Silicon / Intel 区分)| ✅ | ❌ | ❌ | ❌ |
36
+ | SSR Client Hints | ✅ | ❌ | ❌ | ❌ |
37
+ | 无头浏览器检测 | ✅ | ❌ | ❌ | ❌ |
38
+ | AI 爬虫识别 | ✅ 40+ | ❌ | ❌ | ❌ |
39
+ | 设备类型(TV / Console / XR)| ✅ | ❌ | ❌ | ❌ |
40
+
24
41
  ## 安装
25
42
 
26
43
  ```sh
@@ -62,7 +79,25 @@ console.log(info)
62
79
 
63
80
  ## 使用
64
81
 
65
- ### 浏览器环境
82
+ ### 浏览器环境(推荐:`detect`)
83
+
84
+ 使用 `detect()` 获得精准设备与架构信息 —— 在 UA 解析的基础上额外采集硬件信号:
85
+
86
+ ```typescript
87
+ import uaBrowser from 'ua-browser'
88
+
89
+ const result = await uaBrowser.detect()
90
+ console.log(result.device) // 'Mobile' —— 即使开了桌面模式也正确
91
+ console.log(result.arch) // 'arm64' 或 'x86_64'
92
+
93
+ if (result.device === 'Mobile') {
94
+ // 跳转移动版
95
+ }
96
+ ```
97
+
98
+ > **注意**:`detect()` 内部调用 Client Hints 高熵 API(`getHighEntropyValues`),该 API 仅在 **HTTPS 或 localhost** 环境下可用。HTTP 页面中调用时会静默降级,浏览器版本和 OS 版本将退回 UA 字符串的冻结值(如 Chrome 版本显示为 `149.0.0.0`,macOS 26+ 显示为 `10.15.7`)。
99
+
100
+ ### 浏览器环境(同步:`uaBrowser`)
66
101
 
67
102
  ```typescript
68
103
  import uaBrowser from 'ua-browser'
@@ -205,6 +240,28 @@ import {
205
240
  - **AI 爬虫** — GPTBot、ClaudeBot、PerplexityBot、CCBot;消息应用 Bot(Slack、Discord、Telegram、WhatsApp)等
206
241
  - **设备** — Mobile、Tablet、PC、TV(含三星 Smart TV、HbbTV 标准)、Console(PS5、Xbox、Switch)、XR(Vision Pro、Quest)
207
242
 
243
+ ## 常见问题
244
+
245
+ **和 ua-parser-js 有什么区别?**
246
+
247
+ `ua-parser-js` 专注于 UA 字符串本身的解析,不具备硬件信号采集能力;在手机开启桌面模式或 UA 被篡改时会给出错误结果。ua-browser 额外引入 WebGL 渲染器、Client Hints、CSS `safe-area-inset` 等多维信号,并内置 40+ AI 爬虫识别规则和无头浏览器检测,`ua-parser-js` 均不支持。
248
+
249
+ **在 Next.js / Nuxt 等 SSR 框架里能用吗?**
250
+
251
+ 可以。`parseUA(ua)` 是纯函数,无任何浏览器 API 依赖,可直接在 Node.js / Edge Runtime 中使用。搭配 `parseHeaders()` 和 `ACCEPT_CH` 还可在服务端利用 Client Hints 获取精准的架构与平台信息。
252
+
253
+ **手机开了"请求桌面网站",还能正确识别设备类型吗?**
254
+
255
+ 可以,但需要使用 `uaBrowser.detect()` 或手动调用 `getEnvContext()`。这两种方式会采集 CSS `safe-area-inset`、振动 API、设备像素比等硬件信号,不依赖 UA 字符串里的设备声明。
256
+
257
+ **如何识别 GPT、Claude 等 AI 爬虫的抓取请求?**
258
+
259
+ 读取返回值的 `isBot` 和 `botName` 字段即可。库内置了 GPTBot、ClaudeBot、PerplexityBot、CCBot 等规则,同时也覆盖 Slack、Discord、Telegram 等消息应用的链接预览 Bot。
260
+
261
+ **包体积有多大?**
262
+
263
+ 零运行时依赖,gzip 后极小;按需引入(named exports + tree-shaking)体积更小。
264
+
208
265
  ## License
209
266
 
210
267
  [MIT](./LICENSE) © yangtianxia
package/dist/index.cjs CHANGED
@@ -4,25 +4,24 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  // package.json
6
6
  var package_default = {
7
- version: "1.4.0"};
7
+ version: "1.4.1"};
8
8
 
9
9
  // src/constants/os.ts
10
10
  var OS_DEFS = [
11
- { name: "WebOS", detect: /hpwOS/, versionPattern: /hpwOS\/([\d.]+)/ },
12
- { name: "Symbian", detect: /Symbian/, versionPattern: null },
13
- { name: "MeeGo", detect: /MeeGo/, versionPattern: null },
14
- { name: "BlackBerry", detect: /(BlackBerry|RIM)/, versionPattern: null },
15
- { name: "FreeBSD", detect: /FreeBSD/, versionPattern: null },
16
- { name: "Debian", detect: /Debian/, versionPattern: /Debian\/([\d.]+)/ },
17
- { name: "Ubuntu", detect: /Ubuntu/, versionPattern: null },
18
- // Linux must come before Chrome OS: Chrome OS UAs contain "X11", so Linux matches first,
19
- // then Chrome OS overrides it.
20
- { name: "Linux", detect: /(Linux|X11)/, versionPattern: null },
21
- { name: "Chrome OS", detect: /CrOS/, versionPattern: null },
22
- { name: "Tizen", detect: /Tizen/, versionPattern: /Tizen ([\d.]+)/ },
23
- { name: "iOS", detect: /like Mac OS X/, versionPattern: /OS ([\d_]+) like/ },
11
+ { name: "WebOS", priority: 20, detect: /hpwOS/, versionPattern: /hpwOS\/([\d.]+)/ },
12
+ { name: "Symbian", priority: 20, detect: /Symbian/, versionPattern: null },
13
+ { name: "MeeGo", priority: 20, detect: /MeeGo/, versionPattern: null },
14
+ { name: "BlackBerry", priority: 20, detect: /(BlackBerry|RIM)/, versionPattern: null },
15
+ { name: "FreeBSD", priority: 20, detect: /FreeBSD/, versionPattern: null },
16
+ { name: "Debian", priority: 20, detect: /Debian/, versionPattern: /Debian\/([\d.]+)/ },
17
+ { name: "Ubuntu", priority: 20, detect: /Ubuntu/, versionPattern: null },
18
+ { name: "Linux", priority: 10, detect: /(Linux|X11)/, versionPattern: null },
19
+ { name: "Chrome OS", priority: 30, detect: /CrOS/, versionPattern: null },
20
+ { name: "Tizen", priority: 20, detect: /Tizen/, versionPattern: /Tizen ([\d.]+)/ },
21
+ { name: "iOS", priority: 20, detect: /like Mac OS X/, versionPattern: /OS ([\d_]+) like/ },
24
22
  {
25
23
  name: "MacOS",
24
+ priority: 20,
26
25
  detect: /Macintosh/,
27
26
  versionPattern: /Mac OS X -?([\d_.]+)/,
28
27
  versionNames: {
@@ -40,27 +39,21 @@ var OS_DEFS = [
40
39
  "15": "Sequoia"
41
40
  }
42
41
  },
43
- // visionOS / tvOS must come AFTER iOS: their UAs also contain "like Mac OS X",
44
- // so they need to override iOS via the last-match-wins iteration.
45
- { name: "visionOS", detect: /visionOS/, versionPattern: /visionOS ([\d_]+)/ },
46
- { name: "tvOS", detect: /Apple TV/, versionPattern: /OS ([\d_]+) like/ },
47
- { name: "Android", detect: /(Android|Adr)/, versionPattern: /(?:Android|Adr) ([\d.]+)/ },
48
- // HarmonyOS must come after Android: HarmonyOS UAs include "Android", so Android matches
49
- // first, then HarmonyOS overrides it. versionPattern tries direct extraction first (5.0+
50
- // pure HarmonyOS UAs don't have Android token), then falls back to Android version + lookup.
42
+ { name: "visionOS", priority: 30, detect: /visionOS/, versionPattern: /visionOS ([\d_]+)/ },
43
+ { name: "tvOS", priority: 30, detect: /Apple TV/, versionPattern: /OS ([\d_]+) like/ },
44
+ { name: "Android", priority: 20, detect: /(Android|Adr)/, versionPattern: /(?:Android|Adr) ([\d.]+)/ },
51
45
  {
52
46
  name: "HarmonyOS",
47
+ priority: 30,
53
48
  detect: /HarmonyOS/,
54
49
  versionPattern: [/HarmonyOS[\s/]([\d.]+)/, /Android ([\d.]+)[;)]/],
55
- versionLookup: { "10": "2", "11": "3", "12": "3", "13": "4" }
50
+ versionLookup: { "10": "2", "11": "3", "12": "3", "13": "4", "14": "4" }
56
51
  },
57
- // OpenHarmony (open-source base) must come after HarmonyOS to override any earlier match.
58
- { name: "OpenHarmony", detect: /OpenHarmony/, versionPattern: /OpenHarmony[\s/]([\d.]+)/ },
59
- { name: "KaiOS", detect: /KAIOS/, versionPattern: /KAIOS\/([\d.]+)/ },
60
- // Windows must come before Windows Phone: Windows Phone UAs contain "Windows", so Windows
61
- // matches first, then Windows Phone overrides it.
52
+ { name: "OpenHarmony", priority: 30, detect: /OpenHarmony/, versionPattern: /OpenHarmony[\s/]([\d.]+)/ },
53
+ { name: "KaiOS", priority: 30, detect: /KAIOS/, versionPattern: /KAIOS\/([\d.]+)/ },
62
54
  {
63
55
  name: "Windows",
56
+ priority: 10,
64
57
  detect: /Windows/,
65
58
  versionPattern: /Windows NT ([\d.]+)/,
66
59
  versionLookup: {
@@ -82,7 +75,7 @@ var OS_DEFS = [
82
75
  "11": "Windows 11"
83
76
  }
84
77
  },
85
- { name: "Windows Phone", detect: /(IEMobile|Windows Phone)/, versionPattern: /Windows Phone(?: OS)? ([\d.]+)/ }
78
+ { name: "Windows Phone", priority: 30, detect: /(IEMobile|Windows Phone)/, versionPattern: /Windows Phone(?: OS)? ([\d.]+)/ }
86
79
  ];
87
80
 
88
81
  // src/constants/browsers.ts
@@ -304,9 +297,11 @@ function lookupVersionName(map, version) {
304
297
  }
305
298
  function detectOs(ua, windowsVersion) {
306
299
  let matchedDef = null;
300
+ let bestPriority = -1;
307
301
  for (const def of OS_DEFS) {
308
- if (def.detect.test(ua)) {
302
+ if (def.detect.test(ua) && def.priority > bestPriority) {
309
303
  matchedDef = def;
304
+ bestPriority = def.priority;
310
305
  }
311
306
  }
312
307
  if (!matchedDef) return { os: "unknown", osVersion: "unknown", osVersionName: "unknown" };
@@ -455,7 +450,7 @@ var BOT_DEFS = [
455
450
  { name: "Facebookbot", detect: /(facebookexternalhit|FacebookBot)/, category: "social" },
456
451
  { name: "Twitterbot", detect: /Twitterbot/, category: "social" },
457
452
  { name: "LinkedInBot", detect: /LinkedInBot/, category: "social" },
458
- { name: "PinterestBot", detect: /Pinterest/, category: "social" },
453
+ { name: "PinterestBot", detect: /Pinterestbot/i, category: "social" },
459
454
  // Messaging link preview bots
460
455
  { name: "Slackbot", detect: /Slackbot/, category: "link-preview" },
461
456
  { name: "Discordbot", detect: /Discordbot/, category: "link-preview" },
@@ -584,6 +579,7 @@ var BRAND_TO_BROWSER = [
584
579
  ["Microsoft Edge", "Edge"],
585
580
  ["Opera", "Opera"],
586
581
  ["Vivaldi", "Vivaldi"],
582
+ ["Brave", "Brave"],
587
583
  ["Google Chrome", "Chrome"],
588
584
  ["Chromium", "Chromium"]
589
585
  ];
@@ -648,6 +644,82 @@ function normalizeBCP47(raw) {
648
644
  return p.toUpperCase();
649
645
  }).join("-");
650
646
  }
647
+ var ISO_639_1 = /* @__PURE__ */ new Set([
648
+ "af",
649
+ "am",
650
+ "ar",
651
+ "az",
652
+ "be",
653
+ "bg",
654
+ "bn",
655
+ "bs",
656
+ "ca",
657
+ "cs",
658
+ "cy",
659
+ "da",
660
+ "de",
661
+ "el",
662
+ "en",
663
+ "es",
664
+ "et",
665
+ "eu",
666
+ "fa",
667
+ "fi",
668
+ "fr",
669
+ "ga",
670
+ "gl",
671
+ "gu",
672
+ "he",
673
+ "hi",
674
+ "hr",
675
+ "hu",
676
+ "hy",
677
+ "id",
678
+ "is",
679
+ "it",
680
+ "ja",
681
+ "ka",
682
+ "kk",
683
+ "km",
684
+ "kn",
685
+ "ko",
686
+ "lt",
687
+ "lv",
688
+ "mk",
689
+ "ml",
690
+ "mn",
691
+ "mr",
692
+ "ms",
693
+ "mt",
694
+ "my",
695
+ "nb",
696
+ "ne",
697
+ "nl",
698
+ "no",
699
+ "pa",
700
+ "pl",
701
+ "pt",
702
+ "ro",
703
+ "ru",
704
+ "si",
705
+ "sk",
706
+ "sl",
707
+ "sq",
708
+ "sr",
709
+ "sv",
710
+ "sw",
711
+ "ta",
712
+ "te",
713
+ "th",
714
+ "tl",
715
+ "tr",
716
+ "uk",
717
+ "ur",
718
+ "uz",
719
+ "vi",
720
+ "zh",
721
+ "zu"
722
+ ]);
651
723
  function languageFromUA(ua) {
652
724
  const kwMatch = /\bLanguage\/([a-zA-Z]{2,3}(?:[-_][a-zA-Z]{2,4}){1,2})\b/i.exec(ua);
653
725
  if (kwMatch) return normalizeBCP47(kwMatch[1]);
@@ -657,6 +729,10 @@ function languageFromUA(ua) {
657
729
  const parts = m[1].replace(/_/g, "-").split("-");
658
730
  if (parts.length >= 2) return normalizeBCP47(m[1]);
659
731
  }
732
+ const bare = /[;(]\s*([a-z]{2,3})\s*[;)]/g;
733
+ while ((m = bare.exec(ua)) !== null) {
734
+ if (ISO_639_1.has(m[1])) return m[1];
735
+ }
660
736
  return "unknown";
661
737
  }
662
738
  function parseUA(ua, options = {}) {
@@ -713,20 +789,8 @@ function parseUA(ua, options = {}) {
713
789
  const opVer = (_h = (_g = /OPR\/([\d.]+)/.exec(ua)) != null ? _g : /OPT\/([\d.]+)/.exec(ua)) != null ? _h : /Opera\/([\d.]+)/.exec(ua);
714
790
  version = (_i = opVer == null ? void 0 : opVer[1]) != null ? _i : "unknown";
715
791
  }
716
- if (browser === "Chrome" && /\S+Browser\//.test(ua)) {
717
- const m = /(\S+Browser)\/([\d.]+)/.exec(ua);
718
- if (m) {
719
- browser = m[1];
720
- version = m[2];
721
- }
722
- }
723
- if (browser === "Firefox" && nav) {
724
- try {
725
- if (typeof clientInformation !== "undefined" || typeof u2f === "undefined") {
726
- browser = "Firefox Nightly";
727
- }
728
- } catch (e) {
729
- }
792
+ if (browser === "Firefox" && /Firefox\/[\d.]+a\d/.test(ua)) {
793
+ browser = "Firefox Nightly";
730
794
  }
731
795
  if (os === "iOS" && browser === "Safari") {
732
796
  const m = /Version\/([\d.]+)/.exec(ua);
@@ -769,7 +833,8 @@ function parseUA(ua, options = {}) {
769
833
  }
770
834
  }
771
835
  const { engine, engineVersion } = detectEngine(ua, browser, version);
772
- const versionMajor = parseInt((_l = version.split(".")[0]) != null ? _l : "0", 10) || 0;
836
+ const major = parseInt((_l = version.split(".")[0]) != null ? _l : "", 10);
837
+ const versionMajor = Number.isNaN(major) ? 0 : major;
773
838
  const connectionType = (_p = (_o = (_n = (_m = options.ctx) != null ? _m : options.nav) == null ? void 0 : _n.connection) == null ? void 0 : _o.effectiveType) != null ? _p : "unknown";
774
839
  const finalOsVersionName = os === "MacOS" || os === "Windows" ? (() => {
775
840
  var _a2;
@@ -1010,7 +1075,7 @@ function deriveWindowsVersion2(platformVersion) {
1010
1075
  return isNaN(major) ? null : major >= 13 ? "11" : "10";
1011
1076
  }
1012
1077
  function parseHeaders(headers) {
1013
- var _a, _b, _c, _d, _e;
1078
+ var _a, _b, _c, _d, _e, _f;
1014
1079
  const normalised = {};
1015
1080
  for (const key of Object.keys(headers)) {
1016
1081
  normalised[key.toLowerCase()] = headers[key];
@@ -1027,12 +1092,24 @@ function parseHeaders(headers) {
1027
1092
  const model = unquote(get("sec-ch-ua-model"));
1028
1093
  const platformVersion = unquote(get("sec-ch-ua-platform-version"));
1029
1094
  const platform = (_e = unquote(get("sec-ch-ua-platform"))) != null ? _e : "";
1095
+ const fullVersionListRaw = get("sec-ch-ua-full-version-list");
1096
+ const fullVersionList = [];
1097
+ if (fullVersionListRaw) {
1098
+ const re = /"([^"]+)";v="([^"]+)"/g;
1099
+ let m;
1100
+ while ((m = re.exec(fullVersionListRaw)) !== null) {
1101
+ fullVersionList.push({ brand: m[1], version: m[2] });
1102
+ }
1103
+ }
1030
1104
  const highEntropyData = {};
1031
1105
  if (architecture !== void 0) highEntropyData.architecture = architecture;
1032
1106
  if (bitness !== void 0) highEntropyData.bitness = bitness;
1033
1107
  if (model !== void 0) highEntropyData.model = model;
1034
1108
  if (platformVersion !== void 0) highEntropyData.platformVersion = platformVersion;
1109
+ if (fullVersionList.length > 0) highEntropyData.fullVersionList = fullVersionList;
1035
1110
  const isMobile = get("sec-ch-ua-mobile") === "?1";
1111
+ const secCHUA = (_f = get("sec-ch-ua")) != null ? _f : "";
1112
+ const hasBrave = /"Brave"/.test(secCHUA);
1036
1113
  const windowsVersion = platform === "Windows" ? deriveWindowsVersion2(platformVersion) : null;
1037
1114
  const ctx = {
1038
1115
  userAgent: ua,
@@ -1040,7 +1117,8 @@ function parseHeaders(headers) {
1040
1117
  language,
1041
1118
  maxTouchPoints: isMobile ? 1 : 0,
1042
1119
  highEntropyData: Object.keys(highEntropyData).length > 0 ? highEntropyData : void 0,
1043
- windowsVersion
1120
+ windowsVersion,
1121
+ hasBrave
1044
1122
  };
1045
1123
  return parseUA(ua, { ctx });
1046
1124
  }