@flighthq/useragent 0.1.0

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.
@@ -0,0 +1,3 @@
1
+ export { detectEndianness, parseUserAgentArch, parseUserAgentEngine, parseUserAgentEngineVersion, parseUserAgentKind, parseUserAgentName, parseUserAgentPointerWidth, parseUserAgentRuntime, parseUserAgentVersion, } from './userAgent';
2
+ export { parseUserAgentFormFactor, parseUserAgentOsName, parseUserAgentOsVersion } from './userAgentParse';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,2BAA2B,EAC3B,kBAAkB,EAClB,kBAAkB,EAClB,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { detectEndianness, parseUserAgentArch, parseUserAgentEngine, parseUserAgentEngineVersion, parseUserAgentKind, parseUserAgentName, parseUserAgentPointerWidth, parseUserAgentRuntime, parseUserAgentVersion, } from './userAgent';
2
+ export { parseUserAgentFormFactor, parseUserAgentOsName, parseUserAgentOsVersion } from './userAgentParse';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,2BAA2B,EAC3B,kBAAkB,EAClB,kBAAkB,EAClB,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { PlatformEndianness, PlatformEngine, PlatformKind, PlatformName, PlatformRuntime } from '@flighthq/types';
2
+ export declare function detectEndianness(): PlatformEndianness;
3
+ export declare function parseUserAgentArch(ua: string, uadPlatform?: string): string;
4
+ export declare function parseUserAgentEngine(ua: string): PlatformEngine;
5
+ export declare function parseUserAgentEngineVersion(ua: string, engine: PlatformEngine): string;
6
+ export declare function parseUserAgentKind(name: PlatformName): PlatformKind;
7
+ export declare function parseUserAgentName(ua: string): PlatformName;
8
+ export declare function parseUserAgentPointerWidth(arch: string): 32 | 64 | -1;
9
+ export declare function parseUserAgentRuntime(win: Record<string, unknown> | null | undefined): PlatformRuntime;
10
+ export declare function parseUserAgentVersion(ua: string, name: PlatformName): string;
11
+ //# sourceMappingURL=userAgent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userAgent.d.ts","sourceRoot":"","sources":["../src/userAgent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAKvH,wBAAgB,gBAAgB,IAAI,kBAAkB,CAWrD;AAWD,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAsB3E;AAKD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,CAK/D;AAMD,wBAAgB,2BAA2B,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CA0BtF;AAKD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAGnE;AAID,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,CAO3D;AAID,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAIrE;AAKD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,CAUtG;AAKD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,MAAM,CA4B5E"}
@@ -0,0 +1,185 @@
1
+ // Probe host CPU byte order via a DataView write-then-read.
2
+ // Overwhelmingly 'little' on all modern hardware (x86/x64/arm/arm64/wasm).
3
+ // Returns 'unknown' if ArrayBuffer is unavailable.
4
+ export function detectEndianness() {
5
+ try {
6
+ const buf = new ArrayBuffer(2);
7
+ new Uint16Array(buf)[0] = 0x0102;
8
+ const bytes = new Uint8Array(buf);
9
+ if (bytes[0] === 0x01)
10
+ return 'big';
11
+ if (bytes[0] === 0x02)
12
+ return 'little';
13
+ }
14
+ catch {
15
+ // ArrayBuffer unavailable — treat as unknown.
16
+ }
17
+ return 'unknown';
18
+ }
19
+ // Parse a browser user-agent string into the canonical CPU architecture token.
20
+ // Canonical tokens: 'x64', 'arm64', 'x86', 'arm', 'riscv64', 'mips64', 'mips'. Returns '' when undetectable.
21
+ // Note: arm64/aarch64 must be tested before arm to avoid false-positive partial matches.
22
+ // The high-entropy navigator.userAgentData.architecture API is async and not used here;
23
+ // this function operates on the synchronous UA string, plus an optional UA-CH platform hint.
24
+ //
25
+ // uadPlatform: pass navigator.userAgentData.platform (Chromium UA-CH) when available — it gives
26
+ // cleaner arch detection than the UA string. 'Windows', 'Linux', 'macOS', 'Chrome OS' map to 'x64'
27
+ // conservatively; Apple Silicon (M-series) is indistinguishable from x64 via this hint alone.
28
+ export function parseUserAgentArch(ua, uadPlatform) {
29
+ if (uadPlatform) {
30
+ const p = uadPlatform.toLowerCase();
31
+ if (p.includes('arm'))
32
+ return 'arm64';
33
+ if (p.includes('x86') ||
34
+ p.includes('windows') ||
35
+ p.includes('linux') ||
36
+ p.includes('mac') ||
37
+ p.includes('chrome')) {
38
+ return 'x64';
39
+ }
40
+ }
41
+ if (/arm64|aarch64/i.test(ua))
42
+ return 'arm64';
43
+ if (/arm/i.test(ua))
44
+ return 'arm';
45
+ if (/x86_64|win64|wow64|x64/i.test(ua))
46
+ return 'x64';
47
+ if (/i[3-6]86|x86/i.test(ua))
48
+ return 'x86';
49
+ if (/riscv64/i.test(ua))
50
+ return 'riscv64';
51
+ if (/mips64/i.test(ua))
52
+ return 'mips64';
53
+ if (/mips/i.test(ua))
54
+ return 'mips';
55
+ return '';
56
+ }
57
+ // Parse a browser user-agent string into the canonical PlatformEngine token.
58
+ // Order matters: Edg/Chrome UAs contain 'Safari'; Firefox UAs do not contain 'Chrome'.
59
+ // Returns 'unknown' for unrecognized or native-host UAs.
60
+ export function parseUserAgentEngine(ua) {
61
+ if (/firefox/i.test(ua))
62
+ return 'gecko';
63
+ if (/chrome|chromium|edg|opr|samsung/i.test(ua))
64
+ return 'blink';
65
+ if (/safari|webkit/i.test(ua))
66
+ return 'webkit';
67
+ return 'unknown';
68
+ }
69
+ // Extract the browser/engine version string from a user-agent string.
70
+ // The engine parameter narrows which version pattern to extract.
71
+ // Returns '' when the version is not present or the engine is 'unknown'.
72
+ // The returned string is the raw version token — never parsed for semantics.
73
+ export function parseUserAgentEngineVersion(ua, engine) {
74
+ switch (engine) {
75
+ case 'gecko': {
76
+ // 'Firefox/120.0' → '120.0'
77
+ const m = /firefox\/([\d.]+)/i.exec(ua);
78
+ return m ? m[1] : '';
79
+ }
80
+ case 'blink': {
81
+ // Prefer Edg/OPR over Chrome for Edge/Opera UAs to surface the product version.
82
+ const edg = /edg\/([\d.]+)/i.exec(ua);
83
+ if (edg)
84
+ return edg[1];
85
+ const opr = /opr\/([\d.]+)/i.exec(ua);
86
+ if (opr)
87
+ return opr[1];
88
+ const chrome = /chrome\/([\d.]+)/i.exec(ua);
89
+ return chrome ? chrome[1] : '';
90
+ }
91
+ case 'webkit': {
92
+ // 'Version/16.0' → '16.0' for Safari; fall back to AppleWebKit/<version>.
93
+ const ver = /version\/([\d.]+)/i.exec(ua);
94
+ if (ver)
95
+ return ver[1];
96
+ const wk = /applewebkit\/([\d.]+)/i.exec(ua);
97
+ return wk ? wk[1] : '';
98
+ }
99
+ default:
100
+ return '';
101
+ }
102
+ }
103
+ // Parse a browser user-agent string into the canonical PlatformKind token.
104
+ // Derived from the platform name — mobile OSes yield 'mobile'; everything else yields 'web'.
105
+ // Use parseUserAgentName to get the name, then this function to derive the kind.
106
+ export function parseUserAgentKind(name) {
107
+ if (name === 'ios' || name === 'android')
108
+ return 'mobile';
109
+ return 'web';
110
+ }
111
+ // Parse a browser user-agent string into the canonical PlatformName token.
112
+ // Returns 'web' when no known OS is detected (the web fallback).
113
+ export function parseUserAgentName(ua) {
114
+ if (/android/i.test(ua))
115
+ return 'android';
116
+ if (/iphone|ipad|ipod/i.test(ua))
117
+ return 'ios';
118
+ if (/win/i.test(ua))
119
+ return 'windows';
120
+ if (/mac/i.test(ua))
121
+ return 'macos';
122
+ if (/linux/i.test(ua))
123
+ return 'linux';
124
+ return 'web';
125
+ }
126
+ // Infer the native pointer width in bits from a canonical arch token.
127
+ // Returns 64 for 'x64'/'arm64', 32 for 'x86'/'arm', -1 when unknown (e.g. 'wasm' or '').
128
+ export function parseUserAgentPointerWidth(arch) {
129
+ if (arch === 'x64' || arch === 'arm64')
130
+ return 64;
131
+ if (arch === 'x86' || arch === 'arm')
132
+ return 32;
133
+ return -1;
134
+ }
135
+ // Infer the host shell / runtime environment from browser-global probes.
136
+ // Accepts the window-like object as a parameter for testability; pass `window` in production.
137
+ // Returns 'unknown' when the window-like object is null/undefined (SSR, no-DOM contexts).
138
+ export function parseUserAgentRuntime(win) {
139
+ if (win == null)
140
+ return 'unknown';
141
+ // Electron: window.process?.versions?.electron is set in the renderer process.
142
+ const proc = win.process;
143
+ if (proc?.versions && proc.versions.electron)
144
+ return 'electron';
145
+ // Tauri: window.__TAURI__ is injected by the Tauri runtime.
146
+ if (win.__TAURI__)
147
+ return 'tauri';
148
+ // Capacitor: window.Capacitor is set by the Capacitor bridge.
149
+ if (win.Capacitor)
150
+ return 'capacitor';
151
+ return 'web';
152
+ }
153
+ // Extract the OS version string from a browser user-agent string for the given platform name.
154
+ // Returns the raw dotted version string. Returns '' when the version is not present in the UA.
155
+ // Never parsed for semantics — the caller may use comparePlatformVersions for numeric comparison.
156
+ export function parseUserAgentVersion(ua, name) {
157
+ switch (name) {
158
+ case 'windows': {
159
+ // 'Windows NT 10.0' → '10.0'
160
+ const m = /windows nt ([\d.]+)/i.exec(ua);
161
+ return m ? m[1] : '';
162
+ }
163
+ case 'macos': {
164
+ // 'Mac OS X 10_15_7' or 'Mac OS X 10.15.7' → '10.15.7'
165
+ const m = /mac os x ([\d_.]+)/i.exec(ua);
166
+ return m ? m[1].replace(/_/g, '.') : '';
167
+ }
168
+ case 'ios': {
169
+ // 'CPU OS 17_4_1' or 'CPU iPhone OS 17_4_1' → '17.4.1'
170
+ const m = /cpu(?: iphone)? os ([\d_]+)/i.exec(ua);
171
+ return m ? m[1].replace(/_/g, '.') : '';
172
+ }
173
+ case 'android': {
174
+ // 'Android 14' → '14'
175
+ const m = /android ([\d.]+)/i.exec(ua);
176
+ return m ? m[1] : '';
177
+ }
178
+ case 'linux':
179
+ // Linux does not embed a kernel version in the browser UA string.
180
+ return '';
181
+ default:
182
+ return '';
183
+ }
184
+ }
185
+ //# sourceMappingURL=userAgent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userAgent.js","sourceRoot":"","sources":["../src/userAgent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { DeviceFormFactor } from '@flighthq/types';
2
+ export declare function parseUserAgentFormFactor(ua: string, maxTouchPoints: number): DeviceFormFactor;
3
+ export declare function parseUserAgentOsName(ua: string): string;
4
+ export declare function parseUserAgentOsVersion(ua: string): string;
5
+ //# sourceMappingURL=userAgentParse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userAgentParse.d.ts","sourceRoot":"","sources":["../src/userAgentParse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAqBxD,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,gBAAgB,CAsB7F;AAMD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAYvD;AAID,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAiB1D"}
@@ -0,0 +1,93 @@
1
+ import { DeviceFormFactorCar, DeviceFormFactorDesktop, DeviceFormFactorPhone, DeviceFormFactorTablet, DeviceFormFactorTV, DeviceFormFactorUnknown, DeviceFormFactorWatch, } from '@flighthq/types';
2
+ // UA-string parsers for device identity fields.
3
+ // All functions are pure and side-effect-free — no DOM access, no globals, no state.
4
+ // Used by the @flighthq/device web backend; importable independently for testing or custom backends.
5
+ //
6
+ // Note: UA string parsing is inherently best-effort. Browsers freeze and spoof UAs regularly.
7
+ // These parsers target the most common real-world patterns; they are not exhaustive.
8
+ // Parses a device form factor from a user-agent string and optional touch-point hint.
9
+ // Returns a DeviceFormFactor constant. Never returns '' — falls back to DeviceFormFactorUnknown.
10
+ // maxTouchPoints: pass navigator.maxTouchPoints when available; -1 when unavailable.
11
+ export function parseUserAgentFormFactor(ua, maxTouchPoints) {
12
+ // Automotive / in-vehicle
13
+ if (/android auto|car browser|automotive/i.test(ua))
14
+ return DeviceFormFactorCar;
15
+ // Smart TV / set-top box
16
+ if (/smart[-_]?tv|smarttv|googletv|appletv|hbbtv|netcast|webos.*tv|tizen.*tv|tv safari/i.test(ua)) {
17
+ return DeviceFormFactorTV;
18
+ }
19
+ // Wearable / watch
20
+ if (/watch\s*os|watch[_ ]?kit|wearable/i.test(ua))
21
+ return DeviceFormFactorWatch;
22
+ // Tablet: iPad explicitly; Android without 'Mobile' in UA; Windows tablet
23
+ if (/ipad/i.test(ua))
24
+ return DeviceFormFactorTablet;
25
+ if (/android/i.test(ua) && !/mobile/i.test(ua))
26
+ return DeviceFormFactorTablet;
27
+ if (/tablet\s*pc|silk|kindle fire/i.test(ua))
28
+ return DeviceFormFactorTablet;
29
+ // Phone: known mobile UA tokens
30
+ if (/iphone|ipod|android.*mobile|windows phone|blackberry|bb\d+|mobile safari/i.test(ua)) {
31
+ return DeviceFormFactorPhone;
32
+ }
33
+ // Desktop: known desktop OS tokens
34
+ if (/win(?:dows)?nt|macintosh|mac os x|linux(?!.*android)|x11/i.test(ua))
35
+ return DeviceFormFactorDesktop;
36
+ // Weak desktop signal: no touch points when the UA is otherwise inconclusive
37
+ if (maxTouchPoints === 0)
38
+ return DeviceFormFactorDesktop;
39
+ return DeviceFormFactorUnknown;
40
+ }
41
+ // Parses an OS name from a user-agent string.
42
+ // Returns a canonical OS name or '' when unknown.
43
+ // Common return values: 'Android', 'iOS', 'iPadOS', 'Windows', 'macOS', 'Linux', 'ChromeOS',
44
+ // 'FreeBSD', 'OpenBSD', 'NetBSD'.
45
+ export function parseUserAgentOsName(ua) {
46
+ if (/android/i.test(ua))
47
+ return 'Android';
48
+ if (/ipad/i.test(ua))
49
+ return 'iPadOS';
50
+ if (/iphone|ipod/i.test(ua))
51
+ return 'iOS';
52
+ if (/cros/i.test(ua))
53
+ return 'ChromeOS';
54
+ if (/windows nt|windows phone/i.test(ua))
55
+ return 'Windows';
56
+ if (/macintosh|mac os x/i.test(ua))
57
+ return 'macOS';
58
+ if (/freebsd/i.test(ua))
59
+ return 'FreeBSD';
60
+ if (/openbsd/i.test(ua))
61
+ return 'OpenBSD';
62
+ if (/netbsd/i.test(ua))
63
+ return 'NetBSD';
64
+ if (/linux/i.test(ua))
65
+ return 'Linux';
66
+ return '';
67
+ }
68
+ // Parses an OS version string from a user-agent string.
69
+ // Returns a dotted version string, e.g. '14.0', '10.0', '13.5.1'. Returns '' when not found.
70
+ export function parseUserAgentOsVersion(ua) {
71
+ // Android: "Android 14.0" or "Android 9"
72
+ const android = ua.match(/android\s+([\d.]+)/i);
73
+ if (android)
74
+ return android[1];
75
+ // iOS/iPadOS: "iPhone OS 17_0_1" or "CPU OS 16_0"
76
+ const ios = ua.match(/(?:iphone|ipad|ipod).*?os\s+([\d_]+)/i);
77
+ if (ios)
78
+ return ios[1].replace(/_/g, '.');
79
+ // Windows: "Windows NT 10.0" → "10.0"
80
+ const win = ua.match(/windows nt\s+([\d.]+)/i);
81
+ if (win)
82
+ return win[1];
83
+ // macOS: "Mac OS X 10_15_7" → "10.15.7" or "Mac OS X 13.0"
84
+ const mac = ua.match(/mac os x\s+([\d_.]+)/i);
85
+ if (mac)
86
+ return mac[1].replace(/_/g, '.');
87
+ // ChromeOS: "CrOS x86_64 14541.0.0"
88
+ const cros = ua.match(/cros\s+\S+\s+([\d.]+)/i);
89
+ if (cros)
90
+ return cros[1];
91
+ return '';
92
+ }
93
+ //# sourceMappingURL=userAgentParse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userAgentParse.js","sourceRoot":"","sources":["../src/userAgentParse.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@flighthq/useragent",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/types": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.3.0"
34
+ },
35
+ "description": "Pure user-agent string parsing to device and platform identity tokens.",
36
+ "sideEffects": false
37
+ }
@@ -0,0 +1,304 @@
1
+ import {
2
+ detectEndianness,
3
+ parseUserAgentArch,
4
+ parseUserAgentEngine,
5
+ parseUserAgentEngineVersion,
6
+ parseUserAgentKind,
7
+ parseUserAgentName,
8
+ parseUserAgentPointerWidth,
9
+ parseUserAgentRuntime,
10
+ parseUserAgentVersion,
11
+ } from './userAgent';
12
+
13
+ describe('detectEndianness', () => {
14
+ it('returns a known canonical value', () => {
15
+ expect(['little', 'big', 'unknown']).toContain(detectEndianness());
16
+ });
17
+
18
+ it('returns little on Node.js (x64 host)', () => {
19
+ // Node.js always runs on x64/arm64 hardware which is little-endian.
20
+ expect(detectEndianness()).toBe('little');
21
+ });
22
+ });
23
+
24
+ describe('parseUserAgentArch', () => {
25
+ it('detects arm64 from aarch64 token', () => {
26
+ expect(parseUserAgentArch('Mozilla/5.0 (Linux; aarch64) AppleWebKit/537.36')).toBe('arm64');
27
+ });
28
+
29
+ it('detects arm64 from arm64 token', () => {
30
+ expect(parseUserAgentArch('Mozilla/5.0 (iPhone; CPU arm64)')).toBe('arm64');
31
+ });
32
+
33
+ it('detects x64 from Win64 token', () => {
34
+ expect(parseUserAgentArch('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')).toBe('x64');
35
+ });
36
+
37
+ it('detects x64 from WOW64 token', () => {
38
+ expect(parseUserAgentArch('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36')).toBe('x64');
39
+ });
40
+
41
+ it('detects x64 from x86_64 token', () => {
42
+ expect(parseUserAgentArch('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36')).toBe('x64');
43
+ });
44
+
45
+ it('detects x86 from i686 token', () => {
46
+ expect(parseUserAgentArch('Mozilla/5.0 (X11; Linux i686; rv:109.0)')).toBe('x86');
47
+ });
48
+
49
+ it('detects arm before arm64 does not fire on plain arm token', () => {
50
+ // A UA with only 'arm' (not 'arm64'/'aarch64') should yield 'arm'.
51
+ expect(parseUserAgentArch('Mozilla/5.0 (Linux; armv7l)')).toBe('arm');
52
+ });
53
+
54
+ it('detects riscv64, mips64, and mips tokens', () => {
55
+ expect(parseUserAgentArch('Mozilla/5.0 (Linux; riscv64)')).toBe('riscv64');
56
+ expect(parseUserAgentArch('Mozilla/5.0 (Linux; mips64)')).toBe('mips64');
57
+ expect(parseUserAgentArch('Mozilla/5.0 (Linux; mips)')).toBe('mips');
58
+ });
59
+
60
+ it('prefers the UA-CH platform hint over the UA string', () => {
61
+ expect(parseUserAgentArch('', 'arm')).toBe('arm64');
62
+ expect(parseUserAgentArch('', 'Windows')).toBe('x64');
63
+ expect(parseUserAgentArch('', 'macOS')).toBe('x64');
64
+ expect(parseUserAgentArch('', 'Linux')).toBe('x64');
65
+ expect(parseUserAgentArch('', 'Chrome OS')).toBe('x64');
66
+ });
67
+
68
+ it('falls back to the UA string when the platform hint is inconclusive', () => {
69
+ expect(parseUserAgentArch('Mozilla/5.0 (X11; Linux x86_64)', 'SomeCustomPlatform')).toBe('x64');
70
+ });
71
+
72
+ it('returns empty string when arch is undetectable', () => {
73
+ expect(parseUserAgentArch('Mozilla/5.0 (X11; Linux) AppleWebKit/537.36')).toBe('');
74
+ expect(parseUserAgentArch('SomeCustomBrowser/1.0')).toBe('');
75
+ });
76
+ });
77
+
78
+ describe('parseUserAgentEngine', () => {
79
+ it('returns gecko for Firefox', () => {
80
+ expect(
81
+ parseUserAgentEngine('Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0'),
82
+ ).toBe('gecko');
83
+ });
84
+
85
+ it('returns blink for Chrome', () => {
86
+ expect(
87
+ parseUserAgentEngine(
88
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
89
+ ),
90
+ ).toBe('blink');
91
+ });
92
+
93
+ it('returns blink for Edge (Edg/)', () => {
94
+ expect(
95
+ parseUserAgentEngine(
96
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.133',
97
+ ),
98
+ ).toBe('blink');
99
+ });
100
+
101
+ it('returns webkit for Safari', () => {
102
+ expect(
103
+ parseUserAgentEngine(
104
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15',
105
+ ),
106
+ ).toBe('webkit');
107
+ });
108
+
109
+ it('returns unknown for unrecognized UA', () => {
110
+ expect(parseUserAgentEngine('')).toBe('unknown');
111
+ expect(parseUserAgentEngine('CustomBot/1.0')).toBe('unknown');
112
+ });
113
+ });
114
+
115
+ describe('parseUserAgentEngineVersion', () => {
116
+ it('extracts Firefox version', () => {
117
+ expect(
118
+ parseUserAgentEngineVersion(
119
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0',
120
+ 'gecko',
121
+ ),
122
+ ).toBe('120.0');
123
+ });
124
+
125
+ it('extracts Chrome version', () => {
126
+ expect(
127
+ parseUserAgentEngineVersion(
128
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.109 Safari/537.36',
129
+ 'blink',
130
+ ),
131
+ ).toBe('120.0.6099.109');
132
+ });
133
+
134
+ it('extracts Edge version (Edg/ token takes priority over Chrome/)', () => {
135
+ expect(
136
+ parseUserAgentEngineVersion(
137
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.133',
138
+ 'blink',
139
+ ),
140
+ ).toBe('120.0.2210.133');
141
+ });
142
+
143
+ it('extracts Safari Version/ token', () => {
144
+ expect(
145
+ parseUserAgentEngineVersion(
146
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15',
147
+ 'webkit',
148
+ ),
149
+ ).toBe('16.0');
150
+ });
151
+
152
+ it('returns empty string for unknown engine', () => {
153
+ expect(parseUserAgentEngineVersion('any UA string', 'unknown')).toBe('');
154
+ });
155
+
156
+ it('returns empty string when version is absent', () => {
157
+ expect(parseUserAgentEngineVersion('', 'gecko')).toBe('');
158
+ });
159
+ });
160
+
161
+ describe('parseUserAgentKind', () => {
162
+ it('returns mobile for ios', () => {
163
+ expect(parseUserAgentKind('ios')).toBe('mobile');
164
+ });
165
+
166
+ it('returns mobile for android', () => {
167
+ expect(parseUserAgentKind('android')).toBe('mobile');
168
+ });
169
+
170
+ it('returns web for desktop names', () => {
171
+ expect(parseUserAgentKind('windows')).toBe('web');
172
+ expect(parseUserAgentKind('macos')).toBe('web');
173
+ expect(parseUserAgentKind('linux')).toBe('web');
174
+ });
175
+
176
+ it('returns web for unknown', () => {
177
+ expect(parseUserAgentKind('unknown')).toBe('web');
178
+ });
179
+ });
180
+
181
+ describe('parseUserAgentName', () => {
182
+ it('detects android', () => {
183
+ expect(parseUserAgentName('Mozilla/5.0 (Linux; Android 14; Pixel 8)')).toBe('android');
184
+ });
185
+
186
+ it('detects ios from iPhone', () => {
187
+ expect(parseUserAgentName('Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X)')).toBe('ios');
188
+ });
189
+
190
+ it('detects ios from iPad', () => {
191
+ expect(parseUserAgentName('Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X)')).toBe('ios');
192
+ });
193
+
194
+ it('detects windows', () => {
195
+ expect(parseUserAgentName('Mozilla/5.0 (Windows NT 10.0; Win64; x64)')).toBe('windows');
196
+ });
197
+
198
+ it('detects macos', () => {
199
+ expect(parseUserAgentName('Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0)')).toBe('macos');
200
+ });
201
+
202
+ it('detects linux', () => {
203
+ expect(parseUserAgentName('Mozilla/5.0 (X11; Linux x86_64)')).toBe('linux');
204
+ });
205
+
206
+ it('returns web when no OS is detected', () => {
207
+ expect(parseUserAgentName('')).toBe('web');
208
+ expect(parseUserAgentName('CustomBot/1.0')).toBe('web');
209
+ });
210
+ });
211
+
212
+ describe('parseUserAgentPointerWidth', () => {
213
+ it('returns 64 for x64', () => {
214
+ expect(parseUserAgentPointerWidth('x64')).toBe(64);
215
+ });
216
+
217
+ it('returns 64 for arm64', () => {
218
+ expect(parseUserAgentPointerWidth('arm64')).toBe(64);
219
+ });
220
+
221
+ it('returns 32 for x86', () => {
222
+ expect(parseUserAgentPointerWidth('x86')).toBe(32);
223
+ });
224
+
225
+ it('returns 32 for arm', () => {
226
+ expect(parseUserAgentPointerWidth('arm')).toBe(32);
227
+ });
228
+
229
+ it('returns -1 for wasm', () => {
230
+ expect(parseUserAgentPointerWidth('wasm')).toBe(-1);
231
+ });
232
+
233
+ it('returns -1 for empty string', () => {
234
+ expect(parseUserAgentPointerWidth('')).toBe(-1);
235
+ });
236
+ });
237
+
238
+ describe('parseUserAgentRuntime', () => {
239
+ it('returns unknown when window is null', () => {
240
+ expect(parseUserAgentRuntime(null)).toBe('unknown');
241
+ });
242
+
243
+ it('returns unknown when window is undefined', () => {
244
+ expect(parseUserAgentRuntime(undefined)).toBe('unknown');
245
+ });
246
+
247
+ it('returns web when no host shell globals are present', () => {
248
+ expect(parseUserAgentRuntime({})).toBe('web');
249
+ });
250
+
251
+ it('returns electron when process.versions.electron is present', () => {
252
+ expect(parseUserAgentRuntime({ process: { versions: { electron: '28.0.0' } } })).toBe('electron');
253
+ });
254
+
255
+ it('returns tauri when __TAURI__ is present', () => {
256
+ expect(parseUserAgentRuntime({ __TAURI__: {} })).toBe('tauri');
257
+ });
258
+
259
+ it('returns capacitor when Capacitor is present', () => {
260
+ expect(parseUserAgentRuntime({ Capacitor: {} })).toBe('capacitor');
261
+ });
262
+
263
+ it('prioritizes electron over tauri when both globals are present', () => {
264
+ expect(parseUserAgentRuntime({ __TAURI__: {}, process: { versions: { electron: '28.0.0' } } })).toBe('electron');
265
+ });
266
+ });
267
+
268
+ describe('parseUserAgentVersion', () => {
269
+ it('parses Windows NT version', () => {
270
+ expect(parseUserAgentVersion('Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'windows')).toBe('10.0');
271
+ });
272
+
273
+ it('parses macOS version with underscore separators', () => {
274
+ expect(parseUserAgentVersion('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', 'macos')).toBe('10.15.7');
275
+ });
276
+
277
+ it('parses macOS version with dot separators', () => {
278
+ expect(parseUserAgentVersion('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15.7)', 'macos')).toBe('10.15.7');
279
+ });
280
+
281
+ it('parses iOS version', () => {
282
+ expect(parseUserAgentVersion('Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X)', 'ios')).toBe('17.4.1');
283
+ });
284
+
285
+ it('parses iOS version without iPhone token', () => {
286
+ expect(parseUserAgentVersion('Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X)', 'ios')).toBe('17.0');
287
+ });
288
+
289
+ it('parses Android version', () => {
290
+ expect(parseUserAgentVersion('Mozilla/5.0 (Linux; Android 14; Pixel 8)', 'android')).toBe('14');
291
+ });
292
+
293
+ it('returns empty string for linux (no kernel in UA)', () => {
294
+ expect(parseUserAgentVersion('Mozilla/5.0 (X11; Linux x86_64)', 'linux')).toBe('');
295
+ });
296
+
297
+ it('returns empty string for unknown name', () => {
298
+ expect(parseUserAgentVersion('any UA', 'unknown')).toBe('');
299
+ });
300
+
301
+ it('returns empty string when version is absent', () => {
302
+ expect(parseUserAgentVersion('', 'windows')).toBe('');
303
+ });
304
+ });
@@ -0,0 +1,137 @@
1
+ import {
2
+ DeviceFormFactorCar,
3
+ DeviceFormFactorDesktop,
4
+ DeviceFormFactorPhone,
5
+ DeviceFormFactorTablet,
6
+ DeviceFormFactorTV,
7
+ DeviceFormFactorUnknown,
8
+ DeviceFormFactorWatch,
9
+ } from '@flighthq/types';
10
+
11
+ import { parseUserAgentFormFactor, parseUserAgentOsName, parseUserAgentOsVersion } from './userAgentParse';
12
+
13
+ describe('parseUserAgentFormFactor', () => {
14
+ it('returns Automotive for Android Auto UA', () => {
15
+ expect(parseUserAgentFormFactor('Android Auto', -1)).toBe(DeviceFormFactorCar);
16
+ });
17
+
18
+ it('returns TV for smart TV UAs', () => {
19
+ expect(parseUserAgentFormFactor('Mozilla/5.0 (SmartTV; Linux)', -1)).toBe(DeviceFormFactorTV);
20
+ expect(parseUserAgentFormFactor('Mozilla/5.0 (SMART-TV; LINUX)', -1)).toBe(DeviceFormFactorTV);
21
+ expect(parseUserAgentFormFactor('Dalvik/2.1.0 (Linux; Android; GoogleTV)', -1)).toBe(DeviceFormFactorTV);
22
+ });
23
+
24
+ it('returns Watch for WatchOS UA', () => {
25
+ expect(parseUserAgentFormFactor('Mozilla/5.0 (Watch OS 10.0)', -1)).toBe(DeviceFormFactorWatch);
26
+ expect(parseUserAgentFormFactor('wearable/1.0', -1)).toBe(DeviceFormFactorWatch);
27
+ });
28
+
29
+ it('returns Tablet for iPad UA', () => {
30
+ const ipad = 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X)';
31
+ expect(parseUserAgentFormFactor(ipad, 5)).toBe(DeviceFormFactorTablet);
32
+ });
33
+
34
+ it('returns Tablet for Android tablet UA (no Mobile token)', () => {
35
+ const androidTablet = 'Mozilla/5.0 (Linux; Android 13; SM-T870) AppleWebKit/537.36';
36
+ expect(parseUserAgentFormFactor(androidTablet, 5)).toBe(DeviceFormFactorTablet);
37
+ });
38
+
39
+ it('returns Phone for iPhone UA', () => {
40
+ const iphone = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)';
41
+ expect(parseUserAgentFormFactor(iphone, 5)).toBe(DeviceFormFactorPhone);
42
+ });
43
+
44
+ it('returns Phone for Android mobile UA', () => {
45
+ const android = 'Mozilla/5.0 (Linux; Android 14; Pixel 8) Mobile Safari/537.36';
46
+ expect(parseUserAgentFormFactor(android, 5)).toBe(DeviceFormFactorPhone);
47
+ });
48
+
49
+ it('returns Desktop for Windows UA', () => {
50
+ const win = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36';
51
+ expect(parseUserAgentFormFactor(win, 0)).toBe(DeviceFormFactorDesktop);
52
+ });
53
+
54
+ it('returns Desktop for macOS UA', () => {
55
+ const mac = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/537.36';
56
+ expect(parseUserAgentFormFactor(mac, 0)).toBe(DeviceFormFactorDesktop);
57
+ });
58
+
59
+ it('returns Desktop when maxTouchPoints is 0 and UA is inconclusive', () => {
60
+ expect(parseUserAgentFormFactor('SomeBrowser/1.0', 0)).toBe(DeviceFormFactorDesktop);
61
+ });
62
+
63
+ it('returns Unknown for empty UA with touch points available', () => {
64
+ expect(parseUserAgentFormFactor('', -1)).toBe(DeviceFormFactorUnknown);
65
+ });
66
+ });
67
+
68
+ describe('parseUserAgentOsName', () => {
69
+ it('returns Android for Android UA', () => {
70
+ expect(parseUserAgentOsName('Mozilla/5.0 (Linux; Android 14; Pixel 8)')).toBe('Android');
71
+ });
72
+
73
+ it('returns iPadOS for iPad UA', () => {
74
+ expect(parseUserAgentOsName('Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X)')).toBe('iPadOS');
75
+ });
76
+
77
+ it('returns iOS for iPhone/iPod UA', () => {
78
+ expect(parseUserAgentOsName('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)')).toBe('iOS');
79
+ expect(parseUserAgentOsName('Mozilla/5.0 (iPod touch; CPU iPhone OS 16_0 like Mac OS X)')).toBe('iOS');
80
+ });
81
+
82
+ it('returns Windows for Windows UA', () => {
83
+ expect(parseUserAgentOsName('Mozilla/5.0 (Windows NT 10.0; Win64; x64)')).toBe('Windows');
84
+ });
85
+
86
+ it('returns macOS for Macintosh UA', () => {
87
+ expect(parseUserAgentOsName('Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0)')).toBe('macOS');
88
+ });
89
+
90
+ it('returns ChromeOS for CrOS UA', () => {
91
+ expect(parseUserAgentOsName('Mozilla/5.0 (X11; CrOS x86_64 14541.0.0)')).toBe('ChromeOS');
92
+ });
93
+
94
+ it('returns Linux for Linux UA', () => {
95
+ expect(parseUserAgentOsName('Mozilla/5.0 (X11; Linux x86_64)')).toBe('Linux');
96
+ });
97
+
98
+ it('returns FreeBSD for FreeBSD UA', () => {
99
+ expect(parseUserAgentOsName('Mozilla/5.0 (X11; FreeBSD amd64)')).toBe('FreeBSD');
100
+ });
101
+
102
+ it('returns empty string for unknown UA', () => {
103
+ expect(parseUserAgentOsName('')).toBe('');
104
+ expect(parseUserAgentOsName('CustomBot/1.0')).toBe('');
105
+ });
106
+ });
107
+
108
+ describe('parseUserAgentOsVersion', () => {
109
+ it('parses Android version', () => {
110
+ expect(parseUserAgentOsVersion('Mozilla/5.0 (Linux; Android 14.0; Pixel)')).toBe('14.0');
111
+ expect(parseUserAgentOsVersion('Mozilla/5.0 (Linux; Android 9; SM-G950F)')).toBe('9');
112
+ });
113
+
114
+ it('parses iOS version with underscore notation', () => {
115
+ expect(parseUserAgentOsVersion('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_1 like Mac OS X)')).toBe('17.0.1');
116
+ expect(parseUserAgentOsVersion('Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X)')).toBe('16.0');
117
+ });
118
+
119
+ it('parses Windows NT version', () => {
120
+ expect(parseUserAgentOsVersion('Mozilla/5.0 (Windows NT 10.0; Win64; x64)')).toBe('10.0');
121
+ expect(parseUserAgentOsVersion('Mozilla/5.0 (Windows NT 6.1; WOW64)')).toBe('6.1');
122
+ });
123
+
124
+ it('parses macOS version with underscore and dot notation', () => {
125
+ expect(parseUserAgentOsVersion('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)')).toBe('10.15.7');
126
+ expect(parseUserAgentOsVersion('Mozilla/5.0 (Macintosh; Intel Mac OS X 13.0)')).toBe('13.0');
127
+ });
128
+
129
+ it('parses ChromeOS version', () => {
130
+ expect(parseUserAgentOsVersion('Mozilla/5.0 (X11; CrOS x86_64 14541.0.0)')).toBe('14541.0.0');
131
+ });
132
+
133
+ it('returns empty string for unknown UA', () => {
134
+ expect(parseUserAgentOsVersion('')).toBe('');
135
+ expect(parseUserAgentOsVersion('CustomBot/1.0')).toBe('');
136
+ });
137
+ });