Skip to content

Commit 586267c

Browse files
committed
feat(ext/web): implement navigator.userAgentData
1 parent 8d5904a commit 586267c

File tree

4 files changed

+263
-3
lines changed

4 files changed

+263
-3
lines changed

Cargo.lock

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ rusqlite = { version = "0.36.0", features = ["bundled"] }
4545
serde = { version = "1.0.219", features = ["derive"] }
4646
signal-hook = "0.3.18"
4747
thiserror = "2.0.12"
48-
tokio = { version = "1.45.1", features = ["rt", "sync", "time"] }
48+
tokio = { version = "1.46.0", features = ["rt", "sync", "time"] }
4949
ureq = { version = "3.0.12", features = ["json"] }
5050
url = { version = "2.5.4", features = ["serde", "expose_internals"] }
5151
wgpu = { version = "25.0.2", features = ["wgsl", "webgpu"] }

runtime/src/ext/web/navigator.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,190 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5+
/**
6+
* Brand information for User-Agent Client Hints
7+
*/
8+
interface UADataValues {
9+
brand: string;
10+
version: string;
11+
}
12+
13+
/**
14+
* High entropy values for User-Agent Client Hints
15+
*/
16+
interface UAHighEntropyValues {
17+
architecture?: string;
18+
bitness?: string;
19+
brands?: UADataValues[];
20+
fullVersionList?: UADataValues[];
21+
mobile?: boolean;
22+
model?: string;
23+
platform?: string;
24+
platformVersion?: string;
25+
wow64?: boolean;
26+
formFactor?: string;
27+
}
28+
29+
/**
30+
* NavigatorUAData interface implementation according to User-Agent Client Hints specification
31+
* https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData
32+
*/
33+
class NavigatorUAData {
34+
/**
35+
* Returns an array of brand information containing the browser name and version
36+
*/
37+
get brands(): UADataValues[] {
38+
// Return realistic brand data based on the user agent
39+
const ua = internal_navigator_user_agent();
40+
41+
// Parse version from user agent (simplified approach without regex)
42+
let version = "119.0.0.0";
43+
const chromeIndex = ua.indexOf("Chrome/");
44+
if (chromeIndex !== -1) {
45+
const versionStart = chromeIndex + 7; // Length of "Chrome/"
46+
const versionEnd = ua.indexOf(" ", versionStart);
47+
if (versionEnd !== -1) {
48+
version = ua.substring(versionStart, versionEnd);
49+
} else {
50+
version = ua.substring(versionStart);
51+
}
52+
}
53+
54+
return [
55+
{ brand: "Not A(Brand", version: "99.0.0.0" },
56+
{ brand: "Google Chrome", version: version },
57+
{ brand: "Chromium", version: version },
58+
];
59+
}
60+
61+
/**
62+
* Returns true if the user-agent is running on a mobile device
63+
*/
64+
get mobile(): boolean {
65+
const ua = internal_navigator_user_agent();
66+
return ua.includes("Mobile") || ua.includes("Android") || ua.includes("iPhone") ||
67+
ua.includes("iPad");
68+
}
69+
70+
/**
71+
* Returns the platform brand the user-agent is running on
72+
*/
73+
get platform(): string {
74+
const ua = internal_navigator_user_agent();
75+
76+
if (ua.includes("Windows NT")) {
77+
return "Windows";
78+
}
79+
if (ua.includes("Macintosh")) {
80+
return "macOS";
81+
}
82+
if (ua.includes("Linux")) {
83+
return "Linux";
84+
}
85+
if (ua.includes("Android")) {
86+
return "Android";
87+
}
88+
if (ua.includes("iPhone") || ua.includes("iPad")) {
89+
return "iOS";
90+
}
91+
return "Unknown";
92+
}
93+
94+
/**
95+
* Returns a Promise that resolves with high entropy values
96+
*/
97+
getHighEntropyValues(hints: string[]): Promise<UAHighEntropyValues> {
98+
const ua = internal_navigator_user_agent();
99+
const result: UAHighEntropyValues = {};
100+
101+
for (const hint of hints) {
102+
switch (hint) {
103+
case "architecture":
104+
if (ua.includes("x86_64") || ua.includes("Win64; x64")) {
105+
result.architecture = "x86";
106+
} else if (ua.includes("aarch64") || ua.includes("ARM64")) {
107+
result.architecture = "arm";
108+
} else {
109+
result.architecture = "x86";
110+
}
111+
break;
112+
113+
case "bitness":
114+
result.bitness = ua.includes("Win64") || ua.includes("x86_64") || ua.includes("aarch64") ?
115+
"64" :
116+
"32";
117+
break;
118+
119+
case "brands":
120+
result.brands = this.brands;
121+
break;
122+
123+
case "fullVersionList":
124+
result.fullVersionList = this.brands;
125+
break;
126+
127+
case "mobile":
128+
result.mobile = this.mobile;
129+
break;
130+
131+
case "model":
132+
result.model = "";
133+
break;
134+
135+
case "platform":
136+
result.platform = this.platform;
137+
break;
138+
139+
case "platformVersion":
140+
if (ua.includes("Windows NT 10.0")) {
141+
result.platformVersion = "10.0.0";
142+
} else if (ua.includes("Windows NT 11.0")) {
143+
result.platformVersion = "11.0.0";
144+
} else if (ua.includes("Mac OS X")) {
145+
// Find Mac OS X version without regex
146+
const macIndex = ua.indexOf("Mac OS X ");
147+
if (macIndex !== -1) {
148+
const versionStart = macIndex + 9; // Length of "Mac OS X "
149+
const versionEnd = ua.indexOf(")", versionStart);
150+
if (versionEnd !== -1) {
151+
const macVersion = ua.substring(versionStart, versionEnd);
152+
result.platformVersion = macVersion.replace(/_/g, ".");
153+
} else {
154+
result.platformVersion = "10.15.7";
155+
}
156+
} else {
157+
result.platformVersion = "10.15.7";
158+
}
159+
} else {
160+
result.platformVersion = "0.0.0";
161+
}
162+
break;
163+
164+
case "wow64":
165+
result.wow64 = false;
166+
break;
167+
168+
case "formFactor":
169+
result.formFactor = this.mobile ? "Mobile" : "Desktop";
170+
break;
171+
}
172+
}
173+
174+
return Promise.resolve(result);
175+
}
176+
177+
/**
178+
* Returns a JSON representation of the low entropy properties
179+
*/
180+
toJSON(): { brands: UADataValues[]; mobile: boolean; platform: string; } {
181+
return {
182+
brands: this.brands,
183+
mobile: this.mobile,
184+
platform: this.platform,
185+
};
186+
}
187+
}
188+
5189
/**
6190
* Navigator interface implementation according to HTML specification
7191
* Provides information about the user agent and browser environment
@@ -102,6 +286,13 @@ class AndromedaNavigator {
102286
get vendorSub(): string {
103287
return "";
104288
}
289+
290+
/**
291+
* Returns a NavigatorUAData object for User-Agent Client Hints
292+
*/
293+
get userAgentData(): NavigatorUAData {
294+
return new NavigatorUAData();
295+
}
105296
}
106297

107298
// Attach the navigator object to globalThis

types/global.d.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,51 @@ interface AddEventListenerOptions extends EventListenerOptions {
12991299
signal?: AbortSignal;
13001300
}
13011301

1302+
/**
1303+
* Brand information for User-Agent Client Hints
1304+
*/
1305+
interface UADataValues {
1306+
brand: string;
1307+
version: string;
1308+
}
1309+
1310+
/**
1311+
* High entropy values for User-Agent Client Hints
1312+
*/
1313+
interface UAHighEntropyValues {
1314+
architecture?: string;
1315+
bitness?: string;
1316+
brands?: UADataValues[];
1317+
fullVersionList?: UADataValues[];
1318+
mobile?: boolean;
1319+
model?: string;
1320+
platform?: string;
1321+
platformVersion?: string;
1322+
wow64?: boolean;
1323+
formFactor?: string;
1324+
}
1325+
1326+
/**
1327+
* NavigatorUAData interface for User-Agent Client Hints
1328+
* https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData
1329+
*/
1330+
interface NavigatorUAData {
1331+
/** Returns an array of brand information containing the browser name and version */
1332+
readonly brands: UADataValues[];
1333+
1334+
/** Returns true if the user-agent is running on a mobile device */
1335+
readonly mobile: boolean;
1336+
1337+
/** Returns the platform brand the user-agent is running on */
1338+
readonly platform: string;
1339+
1340+
/** Returns a Promise that resolves with high entropy values */
1341+
getHighEntropyValues(hints: string[]): Promise<UAHighEntropyValues>;
1342+
1343+
/** Returns a JSON representation of the low entropy properties */
1344+
toJSON(): { brands: UADataValues[]; mobile: boolean; platform: string; };
1345+
}
1346+
13021347
/**
13031348
* Navigator interface following the HTML specification
13041349
* https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object
@@ -1330,6 +1375,9 @@ interface Navigator {
13301375

13311376
/** Returns the vendor sub-version */
13321377
readonly vendorSub: string;
1378+
1379+
/** Returns a NavigatorUAData object for User-Agent Client Hints */
1380+
readonly userAgentData: NavigatorUAData;
13331381
}
13341382

13351383
/**

0 commit comments

Comments
 (0)