handmux 0.22.0 → 0.22.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.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +3 -3
- package/public/assets/{index-CzlitgRX.js → index-ClOByNCn.js} +81 -81
- package/public/assets/{index-CRAlQZDz.css → index-rjrtUwoJ.css} +1 -1
- package/public/index.html +2 -2
- package/src/browser/manager.js +37 -11
- package/src/browser/publicProxy.js +23 -1
- package/src/browser/routes.js +9 -0
- package/src/browser/siteVersion.js +124 -0
package/public/index.html
CHANGED
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
@keyframes bootDot { 0%,100% { opacity: .22; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
|
|
49
49
|
@media (prefers-reduced-motion: reduce) { .boot-glow, .boot-dots i { animation: none; opacity: .8; } }
|
|
50
50
|
</style>
|
|
51
|
-
<script type="module" crossorigin src="/assets/index-
|
|
52
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
51
|
+
<script type="module" crossorigin src="/assets/index-ClOByNCn.js"></script>
|
|
52
|
+
<link rel="stylesheet" crossorigin href="/assets/index-rjrtUwoJ.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" crossorigin href="/assets/index-rjrtUwoJ.css"></noscript>
|
|
53
53
|
</head>
|
|
54
54
|
<body>
|
|
55
55
|
<div id="boot-splash" aria-hidden="true">
|
package/src/browser/manager.js
CHANGED
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
hammerheadRebindHeaders,
|
|
12
12
|
installHammerheadRebindLocationCompat,
|
|
13
13
|
} from './hammerheadRedirectCompat.js';
|
|
14
|
+
import {
|
|
15
|
+
applySiteVersionHeaders,
|
|
16
|
+
normalizeSiteVersion,
|
|
17
|
+
siteVersionIdentity,
|
|
18
|
+
siteVersionNavigatorScript,
|
|
19
|
+
} from './siteVersion.js';
|
|
14
20
|
|
|
15
21
|
const defaultHammerhead = importedHammerhead.default || importedHammerhead;
|
|
16
22
|
const POOL_IDLE_CLOSE_MS = 1_000;
|
|
@@ -32,9 +38,10 @@ function isLoopbackUrl(raw) {
|
|
|
32
38
|
return hostname === 'localhost' || classifyIp(hostname) === 'loopback';
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
function bridgeScript(channel) {
|
|
41
|
+
function bridgeScript(channel, identity) {
|
|
36
42
|
const encoded = JSON.stringify(channel);
|
|
37
|
-
return
|
|
43
|
+
return `${siteVersionNavigatorScript(identity)}
|
|
44
|
+
(() => {
|
|
38
45
|
const channel = ${encoded};
|
|
39
46
|
const hammerhead = window['%hammerhead%'];
|
|
40
47
|
const destinationUrl = (url) => {
|
|
@@ -92,7 +99,7 @@ function bridgeScript(channel) {
|
|
|
92
99
|
|
|
93
100
|
function browserSessionClass(hammerhead) {
|
|
94
101
|
return class BrowserSession extends hammerhead.Session {
|
|
95
|
-
constructor(channel) {
|
|
102
|
+
constructor(channel, identity) {
|
|
96
103
|
super([], {
|
|
97
104
|
disablePageCaching: true,
|
|
98
105
|
allowMultipleWindows: true,
|
|
@@ -101,10 +108,11 @@ function browserSessionClass(hammerhead) {
|
|
|
101
108
|
nativeAutomation: false,
|
|
102
109
|
});
|
|
103
110
|
this.channel = channel;
|
|
111
|
+
this.identity = identity;
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
async getPayloadScript() { return bridgeScript(this.channel); }
|
|
107
|
-
async getIframePayloadScript() { return
|
|
114
|
+
async getPayloadScript() { return bridgeScript(this.channel, this.identity); }
|
|
115
|
+
async getIframePayloadScript() { return siteVersionNavigatorScript(this.identity); }
|
|
108
116
|
getAuthCredentials() { return null; }
|
|
109
117
|
handleAttachment() {}
|
|
110
118
|
handleFileDownload() {}
|
|
@@ -148,6 +156,7 @@ function publicLease(lease) {
|
|
|
148
156
|
url: lease.url,
|
|
149
157
|
originalUrl: lease.originalUrl,
|
|
150
158
|
channel: lease.channel,
|
|
159
|
+
siteVersion: lease.siteVersion,
|
|
151
160
|
};
|
|
152
161
|
}
|
|
153
162
|
|
|
@@ -305,7 +314,9 @@ export async function createBrowserPreviewManager({
|
|
|
305
314
|
if (lease.timer != null) clearTimer(lease.timer);
|
|
306
315
|
lease.timer = setTimer(() => release(lease), leaseTtlMs);
|
|
307
316
|
};
|
|
308
|
-
const createLease = async ({
|
|
317
|
+
const createLease = async ({
|
|
318
|
+
tabId, deviceId, url, origin, channel, siteVersion, sourceUserAgent,
|
|
319
|
+
}) => {
|
|
309
320
|
if (closing) throw new Error('browser manager closing');
|
|
310
321
|
const target = normalizedTarget(url);
|
|
311
322
|
const publicOrigin = normalizedOrigin(origin);
|
|
@@ -316,7 +327,8 @@ export async function createBrowserPreviewManager({
|
|
|
316
327
|
let detachCookies = null;
|
|
317
328
|
try {
|
|
318
329
|
if (closing) throw new Error('browser manager closing');
|
|
319
|
-
|
|
330
|
+
const identity = siteVersionIdentity(siteVersion, sourceUserAgent);
|
|
331
|
+
session = new SessionClass(channel, identity);
|
|
320
332
|
session.id = `_browser-${randomId()}-${encodeURIComponent(tabId)}`;
|
|
321
333
|
const policy = targetPolicyFactory({ topLevelUrl: target, handmuxOrigin });
|
|
322
334
|
const hooks = session.requestHookEventProvider || session;
|
|
@@ -325,6 +337,7 @@ export async function createBrowserPreviewManager({
|
|
|
325
337
|
if (await rehomeNavigation(event, session)) return;
|
|
326
338
|
const result = await policy.check(event._requestInfo.url);
|
|
327
339
|
if (result.allowed) {
|
|
340
|
+
applySiteVersionHeaders(event.requestOptions?.headers, identity);
|
|
328
341
|
if (result.addresses?.length && event.requestOptions) {
|
|
329
342
|
const approved = result.addresses.map(({ address, family }) => ({ address, family }));
|
|
330
343
|
event.requestOptions.autoSelectFamily = true;
|
|
@@ -352,6 +365,8 @@ export async function createBrowserPreviewManager({
|
|
|
352
365
|
url: publicUrl,
|
|
353
366
|
publicOrigin,
|
|
354
367
|
channel,
|
|
368
|
+
siteVersion,
|
|
369
|
+
sourceUserAgent,
|
|
355
370
|
pool,
|
|
356
371
|
session,
|
|
357
372
|
policy,
|
|
@@ -367,14 +382,21 @@ export async function createBrowserPreviewManager({
|
|
|
367
382
|
}
|
|
368
383
|
};
|
|
369
384
|
|
|
370
|
-
const putImpl = async ({
|
|
385
|
+
const putImpl = async ({
|
|
386
|
+
tabId, deviceId, url, origin, siteVersion, sourceUserAgent,
|
|
387
|
+
}, requireExisting = false) => {
|
|
371
388
|
if (!deviceId || !tabId) throw new Error('browser lease identity required');
|
|
372
389
|
const key = leaseKey(deviceId, tabId);
|
|
373
390
|
const existing = leases.get(key);
|
|
374
391
|
if (requireExisting && !existing) return null;
|
|
375
392
|
const target = normalizedTarget(url);
|
|
376
393
|
const publicOrigin = normalizedOrigin(origin);
|
|
377
|
-
|
|
394
|
+
const requestedVersion = normalizeSiteVersion(siteVersion);
|
|
395
|
+
if (!requestedVersion) throw new Error('invalid browser site version');
|
|
396
|
+
if (existing
|
|
397
|
+
&& existing.originalUrl === target
|
|
398
|
+
&& existing.publicOrigin === publicOrigin
|
|
399
|
+
&& existing.siteVersion === requestedVersion) {
|
|
378
400
|
touch(existing);
|
|
379
401
|
return publicLease(existing);
|
|
380
402
|
}
|
|
@@ -384,6 +406,8 @@ export async function createBrowserPreviewManager({
|
|
|
384
406
|
url: target,
|
|
385
407
|
origin: publicOrigin,
|
|
386
408
|
channel: existing?.channel || randomChannel(),
|
|
409
|
+
siteVersion: requestedVersion,
|
|
410
|
+
sourceUserAgent,
|
|
387
411
|
});
|
|
388
412
|
leases.set(key, next);
|
|
389
413
|
touch(next);
|
|
@@ -438,6 +462,8 @@ export async function createBrowserPreviewManager({
|
|
|
438
462
|
url: target,
|
|
439
463
|
origin,
|
|
440
464
|
channel: current.channel,
|
|
465
|
+
siteVersion: current.siteVersion,
|
|
466
|
+
sourceUserAgent: current.sourceUserAgent,
|
|
441
467
|
});
|
|
442
468
|
try {
|
|
443
469
|
const bootstrapUrl = browserBootstrap.issue({
|
|
@@ -471,8 +497,8 @@ export async function createBrowserPreviewManager({
|
|
|
471
497
|
|
|
472
498
|
return {
|
|
473
499
|
putLease: (options) => put(options),
|
|
474
|
-
navigateLease(tabId, url, deviceId, origin) {
|
|
475
|
-
return put({ tabId, url, deviceId, origin }, true);
|
|
500
|
+
navigateLease(tabId, url, deviceId, origin, siteVersion, sourceUserAgent) {
|
|
501
|
+
return put({ tabId, url, deviceId, origin, siteVersion, sourceUserAgent }, true);
|
|
476
502
|
},
|
|
477
503
|
getLease(tabId, deviceId) {
|
|
478
504
|
return publicLease(leases.get(leaseKey(deviceId, tabId)));
|
|
@@ -13,6 +13,18 @@ const SERVICE_PATHS = new Set([
|
|
|
13
13
|
]);
|
|
14
14
|
const DEVICE_COOKIE = 'tw_browser_device';
|
|
15
15
|
|
|
16
|
+
function sessionChannel(pathname) {
|
|
17
|
+
const descriptor = String(pathname || '').split('/')[1] || '';
|
|
18
|
+
return descriptor.match(/\*([A-Za-z0-9_-]{1,128})$/)?.[1] || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sessionUnavailableHtml(pathname) {
|
|
22
|
+
const channel = JSON.stringify(sessionChannel(pathname));
|
|
23
|
+
return `<!doctype html><meta charset="utf-8">
|
|
24
|
+
<script>parent.postMessage({source:'handmux-browser',channel:${channel},type:'session-unavailable'},'*')</script>
|
|
25
|
+
<pre>{"error":"browser session unavailable"}</pre>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
export function isBrowserServicePath(pathname) {
|
|
17
29
|
return SERVICE_PATHS.has(String(pathname || '').split('?')[0]);
|
|
18
30
|
}
|
|
@@ -65,6 +77,12 @@ function filteredCookie(raw) {
|
|
|
65
77
|
return values.join('; ');
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
function expectsDocument(req) {
|
|
81
|
+
const destination = String(req.headers['sec-fetch-dest'] || '').toLowerCase();
|
|
82
|
+
const accept = String(req.headers.accept || '').toLowerCase();
|
|
83
|
+
return destination === 'document' || destination === 'iframe' || accept.includes('text/html');
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
function upstreamHeaders(headers, port, token) {
|
|
69
87
|
const out = { ...headers, host: `127.0.0.1:${port}` };
|
|
70
88
|
if (token && out.authorization === `Bearer ${token}`) delete out.authorization;
|
|
@@ -101,7 +119,11 @@ export function createBrowserPublicProxy({
|
|
|
101
119
|
const deviceId = cookieValue(req.headers.cookie, DEVICE_COOKIE);
|
|
102
120
|
const target = browserTarget(browser, req, deviceId);
|
|
103
121
|
if (!target) {
|
|
104
|
-
if (claimedBrowserRequest(req))
|
|
122
|
+
if (claimedBrowserRequest(req)) {
|
|
123
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
124
|
+
if (expectsDocument(req)) return res.status(403).type('html').send(sessionUnavailableHtml(pathname));
|
|
125
|
+
return res.status(403).json({ error: 'browser session unavailable' });
|
|
126
|
+
}
|
|
105
127
|
return next();
|
|
106
128
|
}
|
|
107
129
|
const { port } = target;
|
package/src/browser/routes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { browserLabelForOrigin } from './originLabel.js';
|
|
3
3
|
import { browserRequestOrigin } from './publicProxy.js';
|
|
4
|
+
import { normalizeSiteVersion } from './siteVersion.js';
|
|
4
5
|
|
|
5
6
|
const RETENTION_DAYS = new Set([1, 7, 30, null]);
|
|
6
7
|
const DEVICE_ID = /^[A-Za-z0-9_-]{32,128}$/;
|
|
@@ -71,6 +72,8 @@ export function browserRoutes({
|
|
|
71
72
|
if (!TAB_ID.test(req.params.tabId)) return res.status(400).json({ error: 'bad browser tab id' });
|
|
72
73
|
const url = normalizedTarget(req.body?.url);
|
|
73
74
|
if (!url) return res.status(400).json({ error: 'browser URL must use http or https' });
|
|
75
|
+
const siteVersion = normalizeSiteVersion(req.body?.siteVersion);
|
|
76
|
+
if (!siteVersion) return res.status(400).json({ error: 'bad browser site version' });
|
|
74
77
|
try {
|
|
75
78
|
const origin = wildcardOrigin(publicBase, new URL(url).origin);
|
|
76
79
|
const lease = await browser.putLease({
|
|
@@ -78,6 +81,8 @@ export function browserRoutes({
|
|
|
78
81
|
url: req.body.url,
|
|
79
82
|
origin,
|
|
80
83
|
deviceId: req.browserDeviceId,
|
|
84
|
+
siteVersion,
|
|
85
|
+
sourceUserAgent: req.get('user-agent') || '',
|
|
81
86
|
});
|
|
82
87
|
return res.json(responseLease(lease, req.browserDeviceId));
|
|
83
88
|
} catch (error) {
|
|
@@ -90,6 +95,8 @@ export function browserRoutes({
|
|
|
90
95
|
if (!TAB_ID.test(req.params.tabId)) return res.status(400).json({ error: 'bad browser tab id' });
|
|
91
96
|
const url = normalizedTarget(req.body?.url);
|
|
92
97
|
if (!url) return res.status(400).json({ error: 'browser URL must use http or https' });
|
|
98
|
+
const siteVersion = normalizeSiteVersion(req.body?.siteVersion);
|
|
99
|
+
if (!siteVersion) return res.status(400).json({ error: 'bad browser site version' });
|
|
93
100
|
try {
|
|
94
101
|
const origin = wildcardOrigin(publicBase, new URL(url).origin);
|
|
95
102
|
const lease = await browser.navigateLease(
|
|
@@ -97,6 +104,8 @@ export function browserRoutes({
|
|
|
97
104
|
req.body.url,
|
|
98
105
|
req.browserDeviceId,
|
|
99
106
|
origin,
|
|
107
|
+
siteVersion,
|
|
108
|
+
req.get('user-agent') || '',
|
|
100
109
|
);
|
|
101
110
|
if (!lease) return res.status(404).json({ error: 'browser proxy lease not found' });
|
|
102
111
|
return res.json(responseLease(lease, req.browserDeviceId));
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export const DEFAULT_SITE_VERSION = 'mobile';
|
|
2
|
+
export const SITE_VERSIONS = new Set(['mobile', 'desktop']);
|
|
3
|
+
|
|
4
|
+
export function normalizeSiteVersion(value, fallback = DEFAULT_SITE_VERSION) {
|
|
5
|
+
if (value == null || value === '') return fallback;
|
|
6
|
+
return SITE_VERSIONS.has(value) ? value : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function versionFrom(ua, pattern, fallback = '120.0.0.0') {
|
|
10
|
+
return ua.match(pattern)?.[1] || fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function identityPlatform(ua, mobile) {
|
|
14
|
+
if (/iPhone|iPad|iPod/i.test(ua)) return { navigator: 'iPhone', clientHint: 'iOS' };
|
|
15
|
+
if (/Android/i.test(ua)) return { navigator: 'Linux armv8l', clientHint: 'Android' };
|
|
16
|
+
if (/Windows/i.test(ua)) return { navigator: 'Win32', clientHint: 'Windows' };
|
|
17
|
+
if (/Macintosh|Mac OS X/i.test(ua)) return { navigator: 'MacIntel', clientHint: 'macOS' };
|
|
18
|
+
return mobile
|
|
19
|
+
? { navigator: 'Linux armv8l', clientHint: 'Android' }
|
|
20
|
+
: { navigator: 'Linux x86_64', clientHint: 'Linux' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isMobileUserAgent(ua) {
|
|
24
|
+
return /Android|Mobile|iPhone|iPad|iPod/i.test(ua);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function transformedUserAgent(source, mobile) {
|
|
28
|
+
const ua = String(source || '');
|
|
29
|
+
if (ua && isMobileUserAgent(ua) === mobile) return ua;
|
|
30
|
+
|
|
31
|
+
const webkit = versionFrom(ua, /AppleWebKit\/([\d.]+)/i, '605.1.15');
|
|
32
|
+
const safari = versionFrom(ua, /Version\/([\d.]+)/i, '18.0');
|
|
33
|
+
if (/Safari\//i.test(ua) && !/(?:Chrome|CriOS|Chromium|Edg|OPR)\//i.test(ua)) {
|
|
34
|
+
return mobile
|
|
35
|
+
? `Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/${webkit} (KHTML, like Gecko) Version/${safari} Mobile/15E148 Safari/${webkit}`
|
|
36
|
+
: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/${webkit} (KHTML, like Gecko) Version/${safari} Safari/${webkit}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const firefox = ua.match(/Firefox\/([\d.]+)/i)?.[1];
|
|
40
|
+
if (firefox) {
|
|
41
|
+
return mobile
|
|
42
|
+
? `Mozilla/5.0 (Android 10; Mobile; rv:${firefox}) Gecko/${firefox} Firefox/${firefox}`
|
|
43
|
+
: `Mozilla/5.0 (X11; Linux x86_64; rv:${firefox}) Gecko/20100101 Firefox/${firefox}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const chrome = versionFrom(ua, /(?:Chrome|CriOS|Chromium|Edg|OPR)\/([\d.]+)/i);
|
|
47
|
+
return mobile
|
|
48
|
+
? `Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chrome} Mobile Safari/537.36`
|
|
49
|
+
: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chrome} Safari/537.36`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function siteVersionIdentity(siteVersion, sourceUserAgent = '') {
|
|
53
|
+
const version = normalizeSiteVersion(siteVersion);
|
|
54
|
+
if (!version) throw new Error('invalid browser site version');
|
|
55
|
+
const mobile = version === 'mobile';
|
|
56
|
+
const userAgent = transformedUserAgent(sourceUserAgent, mobile);
|
|
57
|
+
const platform = identityPlatform(userAgent, mobile);
|
|
58
|
+
return {
|
|
59
|
+
version,
|
|
60
|
+
mobile,
|
|
61
|
+
userAgent,
|
|
62
|
+
appVersion: userAgent.replace(/^Mozilla\//, ''),
|
|
63
|
+
platform: platform.navigator,
|
|
64
|
+
clientHintPlatform: platform.clientHint,
|
|
65
|
+
maxTouchPoints: mobile ? 5 : 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function headerKey(headers, name) {
|
|
70
|
+
const lower = name.toLowerCase();
|
|
71
|
+
return Object.keys(headers || {}).find((key) => key.toLowerCase() === lower) || lower;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setHeader(headers, name, value) {
|
|
75
|
+
headers[headerKey(headers, name)] = value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function deleteHeader(headers, name) {
|
|
79
|
+
const key = Object.keys(headers || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
80
|
+
if (key) delete headers[key];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function applySiteVersionHeaders(headers, identity) {
|
|
84
|
+
if (!headers || !identity) return headers;
|
|
85
|
+
setHeader(headers, 'user-agent', identity.userAgent);
|
|
86
|
+
setHeader(headers, 'sec-ch-ua-mobile', identity.mobile ? '?1' : '?0');
|
|
87
|
+
setHeader(headers, 'sec-ch-ua-platform', `"${identity.clientHintPlatform}"`);
|
|
88
|
+
for (const name of [
|
|
89
|
+
'sec-ch-ua-platform-version', 'sec-ch-ua-arch', 'sec-ch-ua-bitness',
|
|
90
|
+
'sec-ch-ua-model', 'sec-ch-ua-wow64', 'sec-ch-ua-full-version',
|
|
91
|
+
'sec-ch-ua-full-version-list',
|
|
92
|
+
]) deleteHeader(headers, name);
|
|
93
|
+
return headers;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function siteVersionNavigatorScript(identity) {
|
|
97
|
+
const encoded = JSON.stringify(identity).replaceAll('<', '\\u003c');
|
|
98
|
+
return `(() => {
|
|
99
|
+
const profile = ${encoded};
|
|
100
|
+
const define = (name, value) => {
|
|
101
|
+
try { Object.defineProperty(navigator, name, { configurable: true, get: () => value }); }
|
|
102
|
+
catch { /* the native value remains available */ }
|
|
103
|
+
};
|
|
104
|
+
define('userAgent', profile.userAgent);
|
|
105
|
+
define('appVersion', profile.appVersion);
|
|
106
|
+
define('platform', profile.platform);
|
|
107
|
+
define('maxTouchPoints', profile.maxTouchPoints);
|
|
108
|
+
const nativeData = navigator.userAgentData;
|
|
109
|
+
if (nativeData) {
|
|
110
|
+
const data = {
|
|
111
|
+
brands: nativeData.brands || [],
|
|
112
|
+
mobile: profile.mobile,
|
|
113
|
+
platform: profile.clientHintPlatform,
|
|
114
|
+
toJSON() { return { brands: this.brands, mobile: this.mobile, platform: this.platform }; },
|
|
115
|
+
async getHighEntropyValues(hints) {
|
|
116
|
+
let values = {};
|
|
117
|
+
try { values = await nativeData.getHighEntropyValues.call(nativeData, hints); } catch { /* optional API */ }
|
|
118
|
+
return { ...values, mobile: profile.mobile, platform: profile.clientHintPlatform };
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
define('userAgentData', data);
|
|
122
|
+
}
|
|
123
|
+
})();`;
|
|
124
|
+
}
|