mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix(browser): keep the viewport UA override in step with the auth-host identity
The CDP Emulation.setUserAgentOverride a viewport preset installs outranks WebContents.setUserAgent for navigator.userAgent and stands across every later navigation, so a preset applied before reaching accounts.google.com pinned navigator.userAgent to the Chrome-shaped preset UA while the request header said Firefox — reintroducing the mismatch this scope removes.
This commit is contained in:
@@ -2897,23 +2897,35 @@ describe('browserManager', () => {
|
||||
})
|
||||
|
||||
describe('setViewportOverride', () => {
|
||||
function makeGuest(id: number): {
|
||||
const GUEST_ELECTRON_UA =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) orca/1.0.0 Chrome/134.0.0.0 Electron/30.0.0 Safari/537.36'
|
||||
const GUEST_CLEAN_UA =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'
|
||||
|
||||
function makeGuest(
|
||||
id: number,
|
||||
url = 'https://example.com/'
|
||||
): {
|
||||
guest: Record<string, unknown>
|
||||
debuggerSendCommand: ReturnType<typeof vi.fn>
|
||||
debuggerIsAttached: ReturnType<typeof vi.fn>
|
||||
debuggerAttach: ReturnType<typeof vi.fn>
|
||||
setGuestUserAgent: (ua: string) => void
|
||||
} {
|
||||
const debuggerSendCommand = vi.fn().mockResolvedValue(undefined)
|
||||
const debuggerIsAttached = vi.fn(() => true)
|
||||
const debuggerAttach = vi.fn()
|
||||
let currentUa = GUEST_ELECTRON_UA
|
||||
const guest = {
|
||||
id,
|
||||
isDestroyed: vi.fn(() => false),
|
||||
getType: vi.fn(() => 'webview'),
|
||||
getUserAgent: vi.fn(
|
||||
() =>
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) orca/1.0.0 Chrome/134.0.0.0 Electron/30.0.0 Safari/537.36'
|
||||
),
|
||||
getURL: vi.fn(() => url),
|
||||
getUserAgent: vi.fn(() => currentUa),
|
||||
setUserAgent: vi.fn((ua: string) => {
|
||||
currentUa = ua
|
||||
}),
|
||||
session: { getUserAgent: vi.fn(() => GUEST_ELECTRON_UA) },
|
||||
setBackgroundThrottling: guestSetBackgroundThrottlingMock,
|
||||
setWindowOpenHandler: guestSetWindowOpenHandlerMock,
|
||||
on: guestOnMock,
|
||||
@@ -2926,7 +2938,15 @@ describe('browserManager', () => {
|
||||
sendCommand: debuggerSendCommand
|
||||
}
|
||||
}
|
||||
return { guest, debuggerSendCommand, debuggerIsAttached, debuggerAttach }
|
||||
return {
|
||||
guest,
|
||||
debuggerSendCommand,
|
||||
debuggerIsAttached,
|
||||
debuggerAttach,
|
||||
setGuestUserAgent: (ua: string) => {
|
||||
currentUa = ua
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('returns false when the tab is not registered', async () => {
|
||||
@@ -3012,6 +3032,160 @@ describe('browserManager', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Why: the CDP override outranks setUserAgent for navigator.userAgent, so a preset applied on an
|
||||
// auth host must carry the same Firefox identity the header hook sends (verified against real
|
||||
// Electron 43: Emulation.setUserAgentOverride wins over WebContents.setUserAgent and stands
|
||||
// across every later navigation until explicitly cleared).
|
||||
it.each([false, true])(
|
||||
'presents the Firefox UA for a preset applied on a Google auth host (mobile=%s)',
|
||||
async (mobile) => {
|
||||
const { guest, debuggerSendCommand } = makeGuest(
|
||||
mobile ? 4246 : 4245,
|
||||
'https://accounts.google.com/v3/signin/identifier'
|
||||
)
|
||||
webContentsFromIdMock.mockReturnValue(guest)
|
||||
browserManager.attachGuestPolicies(guest as never)
|
||||
browserManager.registerGuest({
|
||||
browserPageId: `tab-auth-${mobile}`,
|
||||
webContentsId: guest.id as number,
|
||||
rendererWebContentsId
|
||||
})
|
||||
|
||||
await browserManager.setViewportOverride(`tab-auth-${mobile}`, {
|
||||
width: mobile ? 375 : 1024,
|
||||
height: mobile ? 667 : 768,
|
||||
deviceScaleFactor: mobile ? 2 : 1,
|
||||
mobile
|
||||
})
|
||||
|
||||
expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', {
|
||||
userAgent: googleAuthUserAgent()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('re-issues the standing UA override when navigating onto and back off an auth host', async () => {
|
||||
const { guest, debuggerSendCommand } = makeGuest(4247)
|
||||
webContentsFromIdMock.mockReturnValue(guest)
|
||||
browserManager.attachGuestPolicies(guest as never)
|
||||
browserManager.registerGuest({
|
||||
browserPageId: 'tab-auth-nav',
|
||||
webContentsId: guest.id as number,
|
||||
rendererWebContentsId
|
||||
})
|
||||
const didStartNavigation = guestOnMock.mock.calls.find(
|
||||
([event]) => event === 'did-start-navigation'
|
||||
)?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void
|
||||
|
||||
// Case A: the desktop preset lands first, while the tab is still off the auth host.
|
||||
await browserManager.setViewportOverride('tab-auth-nav', {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
deviceScaleFactor: 1,
|
||||
mobile: false
|
||||
})
|
||||
expect(debuggerSendCommand).toHaveBeenLastCalledWith('Emulation.setUserAgentOverride', {
|
||||
userAgent: GUEST_CLEAN_UA
|
||||
})
|
||||
|
||||
// Navigating to the auth host must move the standing override to the Firefox identity.
|
||||
debuggerSendCommand.mockClear()
|
||||
didStartNavigation(null, 'https://accounts.google.com/v3/signin/identifier', false, true)
|
||||
await Promise.resolve()
|
||||
expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', {
|
||||
userAgent: googleAuthUserAgent()
|
||||
})
|
||||
|
||||
// Leaving the auth host restores the clean Chrome-shaped preset UA.
|
||||
debuggerSendCommand.mockClear()
|
||||
didStartNavigation(null, 'https://example.com/', false, true)
|
||||
await Promise.resolve()
|
||||
expect(debuggerSendCommand).toHaveBeenCalledWith('Emulation.setUserAgentOverride', {
|
||||
userAgent: GUEST_CLEAN_UA
|
||||
})
|
||||
})
|
||||
|
||||
it('does not touch the UA override on navigation when no preset is standing', async () => {
|
||||
const { guest, debuggerSendCommand } = makeGuest(4248)
|
||||
webContentsFromIdMock.mockReturnValue(guest)
|
||||
browserManager.attachGuestPolicies(guest as never)
|
||||
browserManager.registerGuest({
|
||||
browserPageId: 'tab-no-preset',
|
||||
webContentsId: guest.id as number,
|
||||
rendererWebContentsId
|
||||
})
|
||||
const didStartNavigation = guestOnMock.mock.calls.find(
|
||||
([event]) => event === 'did-start-navigation'
|
||||
)?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void
|
||||
|
||||
didStartNavigation(null, 'https://accounts.google.com/', false, true)
|
||||
await Promise.resolve()
|
||||
expect(debuggerSendCommand).not.toHaveBeenCalledWith(
|
||||
'Emulation.setUserAgentOverride',
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
|
||||
it('stops re-issuing the UA override once the preset is cleared', async () => {
|
||||
const { guest, debuggerSendCommand } = makeGuest(4249)
|
||||
webContentsFromIdMock.mockReturnValue(guest)
|
||||
browserManager.attachGuestPolicies(guest as never)
|
||||
browserManager.registerGuest({
|
||||
browserPageId: 'tab-cleared-preset',
|
||||
webContentsId: guest.id as number,
|
||||
rendererWebContentsId
|
||||
})
|
||||
const didStartNavigation = guestOnMock.mock.calls.find(
|
||||
([event]) => event === 'did-start-navigation'
|
||||
)?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void
|
||||
|
||||
await browserManager.setViewportOverride('tab-cleared-preset', {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
deviceScaleFactor: 1,
|
||||
mobile: false
|
||||
})
|
||||
await browserManager.setViewportOverride('tab-cleared-preset', null)
|
||||
|
||||
debuggerSendCommand.mockClear()
|
||||
didStartNavigation(null, 'https://accounts.google.com/', false, true)
|
||||
await Promise.resolve()
|
||||
expect(debuggerSendCommand).not.toHaveBeenCalledWith(
|
||||
'Emulation.setUserAgentOverride',
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves the UA override alone on navigation for native-UA profiles', async () => {
|
||||
const { guest, debuggerSendCommand } = makeGuest(4250)
|
||||
webContentsFromIdMock.mockReturnValue(guest)
|
||||
browserManager.attachGuestPolicies(guest as never)
|
||||
browserManager.registerGuest({
|
||||
browserPageId: 'tab-native-nav',
|
||||
sessionProfileId: 'native-profile',
|
||||
userAgentMode: 'native',
|
||||
webContentsId: guest.id as number,
|
||||
rendererWebContentsId
|
||||
})
|
||||
const didStartNavigation = guestOnMock.mock.calls.find(
|
||||
([event]) => event === 'did-start-navigation'
|
||||
)?.[1] as (event: unknown, url: string, isInPlace: boolean, isMainFrame: boolean) => void
|
||||
|
||||
await browserManager.setViewportOverride('tab-native-nav', {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
deviceScaleFactor: 1,
|
||||
mobile: false
|
||||
})
|
||||
debuggerSendCommand.mockClear()
|
||||
didStartNavigation(null, 'https://accounts.google.com/', false, true)
|
||||
await Promise.resolve()
|
||||
expect(debuggerSendCommand).not.toHaveBeenCalledWith(
|
||||
'Emulation.setUserAgentOverride',
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
|
||||
it('clears device metrics and disables touch for override=null', async () => {
|
||||
const { guest, debuggerSendCommand } = makeGuest(4343)
|
||||
webContentsFromIdMock.mockReturnValue(guest)
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
} from './browser-clicked-link-routing'
|
||||
import { cleanElectronUserAgent } from './browser-session-ua'
|
||||
import { googleAuthUserAgent, isGoogleAuthUrl } from './browser-google-auth-ua'
|
||||
import { buildViewportUserAgentOverride } from './browser-viewport-user-agent'
|
||||
import type {
|
||||
BrowserViewportOverride,
|
||||
BrowserCertificateFailure,
|
||||
@@ -131,16 +132,6 @@ function isAutomationVisibilityToken(token: unknown): token is string {
|
||||
return typeof token === 'string' && token.length > 0
|
||||
}
|
||||
|
||||
// Why: responsive sites UA-sniff; this is Chrome DevTools' default iPhone UA template with the real Chrome major spliced in to keep sec-ch-ua consistent (see setupClientHintsOverride).
|
||||
function buildMobileUserAgent(chromeMajor: string): string {
|
||||
return `Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/${chromeMajor}.0.0.0 Mobile/15E148 Safari/604.1`
|
||||
}
|
||||
|
||||
function extractChromeMajor(ua: string): string {
|
||||
const match = ua.match(/Chrome\/(\d+)/)
|
||||
return match ? match[1] : '134'
|
||||
}
|
||||
|
||||
export type BrowserGuestRegistration = {
|
||||
browserPageId?: string
|
||||
browserTabId?: string
|
||||
@@ -232,6 +223,9 @@ export class BrowserManager {
|
||||
private readonly rendererWebContentsIdByTabId = new Map<string, number>()
|
||||
// Why: serialize per-tab setViewportOverride so rapid toggles don't interleave CDP commands and leave emulation in a wrong state.
|
||||
private readonly viewportOpsByTabId = new Map<string, Promise<unknown>>()
|
||||
// Why: presence means a CDP UA override is standing for the tab (value = the preset's mobile flag),
|
||||
// so navigation can re-issue it against the target URL's identity.
|
||||
private readonly viewportUaOverrideMobileByTabId = new Map<string, boolean>()
|
||||
private readonly contextMenuCleanupByTabId = new Map<string, () => void>()
|
||||
private readonly grabShortcutCleanupByTabId = new Map<string, () => void>()
|
||||
private readonly shortcutForwardingCleanupByTabId = new Map<string, () => void>()
|
||||
@@ -937,6 +931,43 @@ export class BrowserManager {
|
||||
// navigation never touches the session UA.
|
||||
guest.setUserAgent(guest.session.getUserAgent())
|
||||
}
|
||||
if (browserPageId) {
|
||||
this.reapplyViewportUserAgentOverride(guest, browserPageId, url)
|
||||
}
|
||||
}
|
||||
|
||||
// Why: Emulation.setUserAgentOverride is set once and stands across every later navigation,
|
||||
// outranking setUserAgent for navigator.userAgent. A viewport preset applied before reaching an
|
||||
// auth host would otherwise pin navigator.userAgent to the Chrome-shaped preset UA while the
|
||||
// request header says Firefox — the two-layer disagreement this scope exists to remove.
|
||||
private reapplyViewportUserAgentOverride(
|
||||
guest: Electron.WebContents,
|
||||
browserTabId: string,
|
||||
url: string
|
||||
): void {
|
||||
const mobile = this.viewportUaOverrideMobileByTabId.get(browserTabId)
|
||||
if (mobile === undefined) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (!guest.debugger.isAttached()) {
|
||||
return
|
||||
}
|
||||
// Why: navigator.userAgent is read by page JS after commit, so landing this override
|
||||
// asynchronously still beats every reader; the header layer already forces Firefox on the wire.
|
||||
void guest.debugger
|
||||
.sendCommand(
|
||||
'Emulation.setUserAgentOverride',
|
||||
buildViewportUserAgentOverride({
|
||||
url,
|
||||
mobile,
|
||||
baseUserAgent: cleanElectronUserAgent(guest.getUserAgent())
|
||||
})
|
||||
)
|
||||
.catch(() => {})
|
||||
} catch {
|
||||
// guest may be mid-teardown or the debugger taken over by DevTools
|
||||
}
|
||||
}
|
||||
|
||||
private createPopupChildWindowWithOriginBar(
|
||||
@@ -1120,6 +1151,7 @@ export class BrowserManager {
|
||||
this.worktreeIdByTabId.delete(browserTabId)
|
||||
// Why: drop the viewport-op chain so the Map doesn't retain a promise keyed to a destroyed guest.
|
||||
this.viewportOpsByTabId.delete(browserTabId)
|
||||
this.viewportUaOverrideMobileByTabId.delete(browserTabId)
|
||||
this.annotationViewportBridgeOpsByTabId.delete(browserTabId)
|
||||
}
|
||||
|
||||
@@ -1185,6 +1217,7 @@ export class BrowserManager {
|
||||
this.worktreeIdByTabId.clear()
|
||||
this.sessionProfileIdByPageId.clear()
|
||||
this.userAgentModeByPageId.clear()
|
||||
this.viewportUaOverrideMobileByTabId.clear()
|
||||
this.pendingLoadFailuresByGuestId.clear()
|
||||
this.loadErrorsByGuestId.clear()
|
||||
this.clearedLoadErrorsByGuestId.clear()
|
||||
@@ -1578,38 +1611,20 @@ export class BrowserManager {
|
||||
})
|
||||
// Why: viewport sizing must not override a profile's explicit native-UA identity.
|
||||
if (this.userAgentModeByPageId.get(browserTabId) !== 'native') {
|
||||
if (override.mobile) {
|
||||
const chromeMajor = extractChromeMajor(cleanElectronUserAgent(guest.getUserAgent()))
|
||||
// Why: userAgentMetadata must accompany the mobile UA so client hints match, or bot-detection flags the desktop-hint leak.
|
||||
await dbg.sendCommand('Emulation.setUserAgentOverride', {
|
||||
userAgent: buildMobileUserAgent(chromeMajor),
|
||||
userAgentMetadata: {
|
||||
brands: [
|
||||
{ brand: 'Google Chrome', version: chromeMajor },
|
||||
{ brand: 'Chromium', version: chromeMajor },
|
||||
{ brand: 'Not/A)Brand', version: '24' }
|
||||
],
|
||||
fullVersionList: [
|
||||
{ brand: 'Google Chrome', version: `${chromeMajor}.0.0.0` },
|
||||
{ brand: 'Chromium', version: `${chromeMajor}.0.0.0` },
|
||||
{ brand: 'Not/A)Brand', version: '24.0.0.0' }
|
||||
],
|
||||
fullVersion: `${chromeMajor}.0.0.0`,
|
||||
platform: 'iOS',
|
||||
platformVersion: '17.0',
|
||||
architecture: '',
|
||||
model: 'iPhone',
|
||||
mobile: true
|
||||
}
|
||||
// Why: remember the standing override so later navigations can re-issue it; it outranks
|
||||
// setUserAgent and would otherwise pin the auth-host identity to whatever was set here.
|
||||
this.viewportUaOverrideMobileByTabId.set(browserTabId, override.mobile)
|
||||
await dbg.sendCommand(
|
||||
'Emulation.setUserAgentOverride',
|
||||
buildViewportUserAgentOverride({
|
||||
url: guest.getURL(),
|
||||
mobile: override.mobile,
|
||||
baseUserAgent: cleanElectronUserAgent(guest.getUserAgent())
|
||||
})
|
||||
} else {
|
||||
// Why: desktop presets still need the clean (non-Electron) UA so Cloudflare/Turnstile don't flag the session.
|
||||
await dbg.sendCommand('Emulation.setUserAgentOverride', {
|
||||
userAgent: cleanElectronUserAgent(guest.getUserAgent())
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this.viewportUaOverrideMobileByTabId.delete(browserTabId)
|
||||
await dbg.sendCommand('Emulation.clearDeviceMetricsOverride', {})
|
||||
await dbg.sendCommand('Emulation.setTouchEmulationEnabled', {
|
||||
enabled: false,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { googleAuthUserAgent } from './browser-google-auth-ua'
|
||||
import { buildViewportUserAgentOverride } from './browser-viewport-user-agent'
|
||||
|
||||
const CHROME_UA =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'
|
||||
|
||||
describe('buildViewportUserAgentOverride', () => {
|
||||
it('presents the Firefox UA on Google auth hosts regardless of the preset', () => {
|
||||
for (const mobile of [false, true]) {
|
||||
const override = buildViewportUserAgentOverride({
|
||||
url: 'https://accounts.google.com/v3/signin/identifier',
|
||||
mobile,
|
||||
baseUserAgent: CHROME_UA
|
||||
})
|
||||
expect(override.userAgent).toBe(googleAuthUserAgent())
|
||||
// Real Firefox emits no client hints, so Chrome brands would contradict the stripped headers.
|
||||
expect(override.userAgentMetadata).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the clean desktop UA off the auth hosts', () => {
|
||||
const override = buildViewportUserAgentOverride({
|
||||
url: 'https://myaccount.google.com/',
|
||||
mobile: false,
|
||||
baseUserAgent: CHROME_UA
|
||||
})
|
||||
expect(override.userAgent).toBe(CHROME_UA)
|
||||
expect(override.userAgentMetadata).toBeUndefined()
|
||||
})
|
||||
|
||||
it('splices the real Chrome major into the mobile UA and its client hints', () => {
|
||||
const override = buildViewportUserAgentOverride({
|
||||
url: 'https://example.com/',
|
||||
mobile: true,
|
||||
baseUserAgent: CHROME_UA
|
||||
})
|
||||
expect(override.userAgent).toContain('iPhone')
|
||||
expect(override.userAgent).toContain('CriOS/134.0.0.0')
|
||||
expect(override.userAgentMetadata?.mobile).toBe(true)
|
||||
expect(override.userAgentMetadata?.platform).toBe('iOS')
|
||||
expect(override.userAgentMetadata?.brands).toContainEqual({
|
||||
brand: 'Google Chrome',
|
||||
version: '134'
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to a known Chrome major when the base UA carries none', () => {
|
||||
const override = buildViewportUserAgentOverride({
|
||||
url: 'https://example.com/',
|
||||
mobile: true,
|
||||
baseUserAgent: googleAuthUserAgent()
|
||||
})
|
||||
expect(override.userAgent).toContain('CriOS/134.0.0.0')
|
||||
})
|
||||
|
||||
it('treats an unparseable URL as a non-auth host', () => {
|
||||
const override = buildViewportUserAgentOverride({
|
||||
url: 'not a url',
|
||||
mobile: false,
|
||||
baseUserAgent: CHROME_UA
|
||||
})
|
||||
expect(override.userAgent).toBe(CHROME_UA)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
// Why: a CDP Emulation.setUserAgentOverride outranks WebContents.setUserAgent for both
|
||||
// navigator.userAgent and the outgoing request header, and it stands across every later
|
||||
// navigation until explicitly cleared. So the viewport preset's UA is a third identity layer
|
||||
// that must agree with the auth-host Firefox switch, or applying a preset silently reintroduces
|
||||
// the exact UA mismatch this scope exists to remove.
|
||||
|
||||
import { googleAuthUserAgent, isGoogleAuthUrl } from './browser-google-auth-ua'
|
||||
|
||||
type UserAgentBrand = { brand: string; version: string }
|
||||
|
||||
export type ViewportUserAgentOverride = {
|
||||
userAgent: string
|
||||
userAgentMetadata?: {
|
||||
brands: UserAgentBrand[]
|
||||
fullVersionList: UserAgentBrand[]
|
||||
fullVersion: string
|
||||
platform: string
|
||||
platformVersion: string
|
||||
architecture: string
|
||||
model: string
|
||||
mobile: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Why: responsive sites UA-sniff; this is Chrome DevTools' default iPhone UA template with the real
|
||||
// Chrome major spliced in to keep sec-ch-ua consistent (see setupClientHintsOverride).
|
||||
function buildMobileUserAgent(chromeMajor: string): string {
|
||||
return `Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/${chromeMajor}.0.0.0 Mobile/15E148 Safari/604.1`
|
||||
}
|
||||
|
||||
function extractChromeMajor(ua: string): string {
|
||||
const match = ua.match(/Chrome\/(\d+)/)
|
||||
return match ? match[1] : '134'
|
||||
}
|
||||
|
||||
export function buildViewportUserAgentOverride(args: {
|
||||
url: string
|
||||
mobile: boolean
|
||||
baseUserAgent: string
|
||||
}): ViewportUserAgentOverride {
|
||||
if (isGoogleAuthUrl(args.url)) {
|
||||
// Why: match the header-level Firefox switch exactly, and send no userAgentMetadata — real
|
||||
// Firefox emits no client hints, so Chrome brands here would contradict the stripped headers.
|
||||
return { userAgent: googleAuthUserAgent() }
|
||||
}
|
||||
if (!args.mobile) {
|
||||
// Why: desktop presets still need the clean (non-Electron) UA so Cloudflare/Turnstile don't flag the session.
|
||||
return { userAgent: args.baseUserAgent }
|
||||
}
|
||||
const chromeMajor = extractChromeMajor(args.baseUserAgent)
|
||||
// Why: userAgentMetadata must accompany the mobile UA so client hints match, or bot-detection flags the desktop-hint leak.
|
||||
return {
|
||||
userAgent: buildMobileUserAgent(chromeMajor),
|
||||
userAgentMetadata: {
|
||||
brands: [
|
||||
{ brand: 'Google Chrome', version: chromeMajor },
|
||||
{ brand: 'Chromium', version: chromeMajor },
|
||||
{ brand: 'Not/A)Brand', version: '24' }
|
||||
],
|
||||
fullVersionList: [
|
||||
{ brand: 'Google Chrome', version: `${chromeMajor}.0.0.0` },
|
||||
{ brand: 'Chromium', version: `${chromeMajor}.0.0.0` },
|
||||
{ brand: 'Not/A)Brand', version: '24.0.0.0' }
|
||||
],
|
||||
fullVersion: `${chromeMajor}.0.0.0`,
|
||||
platform: 'iOS',
|
||||
platformVersion: '17.0',
|
||||
architecture: '',
|
||||
model: 'iPhone',
|
||||
mobile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user