mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 00:02:34 +00:00
Fix browser webview permissions and wheel zoom (#4849)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { ANTI_DETECTION_SCRIPT } from './anti-detection'
|
||||
|
||||
type PermissionQueryResult = {
|
||||
state: string
|
||||
onchange: null
|
||||
}
|
||||
|
||||
type AntiDetectionContext = {
|
||||
Notification: {
|
||||
permission: string
|
||||
requestPermission: (callback?: (permission: string) => void) => Promise<string>
|
||||
}
|
||||
navigator: {
|
||||
permissions: {
|
||||
query: (descriptor: { name: string }) => Promise<PermissionQueryResult>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createContext(args: {
|
||||
nativeNotificationPermission: string
|
||||
requestedNotificationPermission: string
|
||||
}): AntiDetectionContext & Record<string, unknown> {
|
||||
class Permissions {
|
||||
query(): Promise<PermissionQueryResult> {
|
||||
return Promise.resolve({ state: 'denied', onchange: null })
|
||||
}
|
||||
}
|
||||
|
||||
const Notification = {
|
||||
permission: args.nativeNotificationPermission,
|
||||
requestPermission(callback?: (permission: string) => void): Promise<string> {
|
||||
callback?.(args.requestedNotificationPermission)
|
||||
return Promise.resolve(args.requestedNotificationPermission)
|
||||
}
|
||||
}
|
||||
Object.defineProperty(Notification, 'permission', {
|
||||
configurable: true,
|
||||
get: () => args.nativeNotificationPermission
|
||||
})
|
||||
|
||||
return {
|
||||
Date,
|
||||
Object,
|
||||
Promise,
|
||||
Set,
|
||||
performance: { now: () => 0 },
|
||||
window: {},
|
||||
navigator: {
|
||||
plugins: [],
|
||||
languages: [],
|
||||
permissions: new Permissions()
|
||||
},
|
||||
Permissions,
|
||||
Notification
|
||||
} as AntiDetectionContext & Record<string, unknown>
|
||||
}
|
||||
|
||||
describe('ANTI_DETECTION_SCRIPT', () => {
|
||||
it('reports notification permission as granted after a site permission request succeeds', async () => {
|
||||
const context = createContext({
|
||||
nativeNotificationPermission: 'denied',
|
||||
requestedNotificationPermission: 'granted'
|
||||
})
|
||||
|
||||
runInNewContext(ANTI_DETECTION_SCRIPT, context)
|
||||
|
||||
expect(context.Notification.permission).toBe('default')
|
||||
await expect(context.navigator.permissions.query({ name: 'notifications' })).resolves.toEqual({
|
||||
state: 'prompt',
|
||||
onchange: null
|
||||
})
|
||||
|
||||
await expect(context.Notification.requestPermission()).resolves.toBe('granted')
|
||||
|
||||
expect(context.Notification.permission).toBe('granted')
|
||||
await expect(context.navigator.permissions.query({ name: 'notifications' })).resolves.toEqual({
|
||||
state: 'granted',
|
||||
onchange: null
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves notification permission when Electron already reports a grant', async () => {
|
||||
const context = createContext({
|
||||
nativeNotificationPermission: 'granted',
|
||||
requestedNotificationPermission: 'granted'
|
||||
})
|
||||
|
||||
runInNewContext(ANTI_DETECTION_SCRIPT, context)
|
||||
|
||||
expect(context.Notification.permission).toBe('granted')
|
||||
await expect(context.navigator.permissions.query({ name: 'notifications' })).resolves.toEqual({
|
||||
state: 'granted',
|
||||
onchange: null
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -59,12 +59,32 @@ export const ANTI_DETECTION_SCRIPT = `(function() {
|
||||
// but real Chrome returns 'prompt' for ungranted permissions. Returning
|
||||
// 'denied' is a strong bot signal. Override the query result for common
|
||||
// permissions that Turnstile and similar detectors probe.
|
||||
var notificationPermission = 'default';
|
||||
var setNotificationPermission = function(permission) {
|
||||
if (permission === 'granted' || permission === 'denied') {
|
||||
notificationPermission = permission;
|
||||
return permission;
|
||||
}
|
||||
notificationPermission = 'default';
|
||||
return 'default';
|
||||
};
|
||||
var notificationPermissionState = function() {
|
||||
return notificationPermission === 'default' ? 'prompt' : notificationPermission;
|
||||
};
|
||||
try {
|
||||
if (Notification.permission === 'granted') {
|
||||
notificationPermission = 'granted';
|
||||
}
|
||||
} catch {}
|
||||
const promptPerms = new Set([
|
||||
'notifications', 'geolocation', 'camera', 'microphone',
|
||||
'geolocation', 'camera', 'microphone',
|
||||
'midi', 'idle-detection', 'storage-access'
|
||||
]);
|
||||
const origQuery = Permissions.prototype.query;
|
||||
Permissions.prototype.query = function(desc) {
|
||||
if (desc.name === 'notifications') {
|
||||
return Promise.resolve({ state: notificationPermissionState(), onchange: null });
|
||||
}
|
||||
if (promptPerms.has(desc.name)) {
|
||||
return Promise.resolve({ state: 'prompt', onchange: null });
|
||||
}
|
||||
@@ -75,8 +95,25 @@ export const ANTI_DETECTION_SCRIPT = `(function() {
|
||||
// or blocked. Turnstile cross-references this with the Permissions API.
|
||||
try {
|
||||
Object.defineProperty(Notification, 'permission', {
|
||||
get: () => 'default'
|
||||
get: () => notificationPermission
|
||||
});
|
||||
const origRequestPermission = Notification.requestPermission;
|
||||
if (typeof origRequestPermission === 'function') {
|
||||
Notification.requestPermission = function(callback) {
|
||||
var wrappedCallback = typeof callback === 'function'
|
||||
? function(permission) {
|
||||
callback(setNotificationPermission(permission));
|
||||
}
|
||||
: undefined;
|
||||
var result = origRequestPermission.call(Notification, wrappedCallback);
|
||||
if (result && typeof result.then === 'function') {
|
||||
return result.then(function(permission) {
|
||||
return setNotificationPermission(permission);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
// Why: Electron webviews may have an empty languages array. Real Chrome
|
||||
// always has at least one entry. An empty array is an automation signal.
|
||||
|
||||
@@ -9,7 +9,12 @@ vi.mock('electron', () => ({
|
||||
webContents: { fromId: vi.fn() }
|
||||
}))
|
||||
|
||||
import { setupGuestContextMenu, setupGuestShortcutForwarding } from './browser-guest-ui'
|
||||
import {
|
||||
resolveGuestMouseWheelZoomDirection,
|
||||
setupGuestContextMenu,
|
||||
setupGuestMouseWheelZoomForwarding,
|
||||
setupGuestShortcutForwarding
|
||||
} from './browser-guest-ui'
|
||||
|
||||
describe('setupGuestContextMenu', () => {
|
||||
const browserTabId = 'tab-1'
|
||||
@@ -253,6 +258,125 @@ describe('setupGuestContextMenu', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('guest mouse wheel browser zoom', () => {
|
||||
const browserTabId = 'tab-1'
|
||||
let rendererSendMock: ReturnType<typeof vi.fn>
|
||||
let guestOnMock: ReturnType<typeof vi.fn>
|
||||
let guestOffMock: ReturnType<typeof vi.fn>
|
||||
|
||||
function makeGuest() {
|
||||
return {
|
||||
on: guestOnMock,
|
||||
off: guestOffMock
|
||||
} as unknown as Electron.WebContents
|
||||
}
|
||||
|
||||
function makeRenderer() {
|
||||
return { send: rendererSendMock } as unknown as Electron.WebContents
|
||||
}
|
||||
|
||||
function mouseWheel(
|
||||
overrides: Partial<Electron.MouseWheelInputEvent> = {}
|
||||
): Electron.MouseWheelInputEvent {
|
||||
return {
|
||||
type: 'mouseWheel',
|
||||
x: 0,
|
||||
y: 0,
|
||||
deltaY: -120,
|
||||
modifiers: ['ctrl'],
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function triggerBeforeMouse(mouse: Electron.MouseInputEvent): ReturnType<typeof vi.fn> {
|
||||
const handler = guestOnMock.mock.calls.find((call) => call[0] === 'before-mouse-event')?.[1] as
|
||||
| ((event: Electron.Event, mouse: Electron.MouseInputEvent) => void)
|
||||
| undefined
|
||||
expect(handler).toBeTypeOf('function')
|
||||
const preventDefault = vi.fn()
|
||||
handler!({ preventDefault } as unknown as Electron.Event, mouse)
|
||||
return preventDefault
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
rendererSendMock = vi.fn()
|
||||
guestOnMock = vi.fn()
|
||||
guestOffMock = vi.fn()
|
||||
})
|
||||
|
||||
it('resolves ctrl wheel direction from guest mouse input', () => {
|
||||
expect(resolveGuestMouseWheelZoomDirection(mouseWheel({ deltaY: -120 }), 'win32')).toBe('in')
|
||||
expect(resolveGuestMouseWheelZoomDirection(mouseWheel({ deltaY: 120 }), 'linux')).toBe('out')
|
||||
})
|
||||
|
||||
it('allows command wheel only on macOS', () => {
|
||||
const commandWheel = mouseWheel({ modifiers: ['cmd'], deltaY: -120 })
|
||||
|
||||
expect(resolveGuestMouseWheelZoomDirection(commandWheel, 'darwin')).toBe('in')
|
||||
expect(resolveGuestMouseWheelZoomDirection(commandWheel, 'win32')).toBeNull()
|
||||
})
|
||||
|
||||
it('ignores non-zoom wheel input', () => {
|
||||
expect(
|
||||
resolveGuestMouseWheelZoomDirection(mouseWheel({ modifiers: [], deltaY: -120 }), 'linux')
|
||||
).toBeNull()
|
||||
expect(
|
||||
resolveGuestMouseWheelZoomDirection(mouseWheel({ modifiers: ['ctrl', 'alt'] }), 'linux')
|
||||
).toBeNull()
|
||||
expect(
|
||||
resolveGuestMouseWheelZoomDirection(mouseWheel({ modifiers: ['ctrl', 'shift'] }), 'linux')
|
||||
).toBeNull()
|
||||
expect(resolveGuestMouseWheelZoomDirection(mouseWheel({ deltaY: 0 }), 'linux')).toBeNull()
|
||||
expect(
|
||||
resolveGuestMouseWheelZoomDirection(
|
||||
{ type: 'mouseMove', x: 0, y: 0, modifiers: ['ctrl'] },
|
||||
'linux'
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('forwards ctrl wheel to browser page zoom and consumes the guest wheel event', () => {
|
||||
setupGuestMouseWheelZoomForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => makeRenderer()
|
||||
})
|
||||
|
||||
const preventDefault = triggerBeforeMouse(mouseWheel({ deltaY: -120 }))
|
||||
const outPreventDefault = triggerBeforeMouse(mouseWheel({ deltaY: 120 }))
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(outPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'ui:zoomBrowserPage', 'in')
|
||||
expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'ui:zoomBrowserPage', 'out')
|
||||
})
|
||||
|
||||
it('consumes guest ctrl wheel even when the renderer is unavailable', () => {
|
||||
setupGuestMouseWheelZoomForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => null
|
||||
})
|
||||
|
||||
const preventDefault = triggerBeforeMouse(mouseWheel())
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(rendererSendMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cleans up the mouse wheel listener on teardown', () => {
|
||||
const cleanup = setupGuestMouseWheelZoomForwarding({
|
||||
browserTabId,
|
||||
guest: makeGuest(),
|
||||
resolveRenderer: () => makeRenderer()
|
||||
})
|
||||
|
||||
cleanup()
|
||||
|
||||
expect(guestOffMock).toHaveBeenCalledWith('before-mouse-event', expect.any(Function))
|
||||
})
|
||||
})
|
||||
|
||||
describe('setupGuestShortcutForwarding', () => {
|
||||
const browserTabId = 'tab-1'
|
||||
let rendererSendMock: ReturnType<typeof vi.fn>
|
||||
|
||||
@@ -14,10 +14,42 @@ import {
|
||||
} from '../../shared/window-shortcut-policy'
|
||||
import { readGuestNavigationState } from './browser-guest-navigation-state'
|
||||
import { keybindingMatchesAction, type KeybindingOverrides } from '../../shared/keybindings'
|
||||
import type { BrowserPageZoomDirection } from '../../shared/browser-page-zoom'
|
||||
|
||||
type ResolveRenderer = (browserTabId: string) => Electron.WebContents | null
|
||||
type ShouldForwardDictationShortcut = () => boolean
|
||||
|
||||
const CONTROL_MODIFIERS = new Set(['control', 'ctrl'])
|
||||
const MAC_COMMAND_MODIFIERS = new Set(['meta', 'command', 'cmd'])
|
||||
const WHEEL_ZOOM_BLOCKING_MODIFIERS = new Set(['alt', 'shift'])
|
||||
|
||||
function hasModifier(mouse: Electron.MouseInputEvent, modifiers: ReadonlySet<string>): boolean {
|
||||
return mouse.modifiers?.some((modifier) => modifiers.has(modifier)) ?? false
|
||||
}
|
||||
|
||||
export function resolveGuestMouseWheelZoomDirection(
|
||||
mouse: Electron.MouseInputEvent,
|
||||
platform: NodeJS.Platform = process.platform
|
||||
): BrowserPageZoomDirection | null {
|
||||
if (mouse.type !== 'mouseWheel') {
|
||||
return null
|
||||
}
|
||||
if (hasModifier(mouse, WHEEL_ZOOM_BLOCKING_MODIFIERS)) {
|
||||
return null
|
||||
}
|
||||
const hasZoomModifier =
|
||||
hasModifier(mouse, CONTROL_MODIFIERS) ||
|
||||
(platform === 'darwin' && hasModifier(mouse, MAC_COMMAND_MODIFIERS))
|
||||
if (!hasZoomModifier) {
|
||||
return null
|
||||
}
|
||||
const deltaY = (mouse as Electron.MouseWheelInputEvent).deltaY
|
||||
if (typeof deltaY !== 'number' || deltaY === 0) {
|
||||
return null
|
||||
}
|
||||
return deltaY < 0 ? 'in' : 'out'
|
||||
}
|
||||
|
||||
function isControlKeyRelease(input: Electron.Input): boolean {
|
||||
return input.type === 'keyUp' && (input.code === 'ControlLeft' || input.code === 'ControlRight')
|
||||
}
|
||||
@@ -424,6 +456,33 @@ export function setupGuestShortcutForwarding(args: {
|
||||
}
|
||||
}
|
||||
|
||||
export function setupGuestMouseWheelZoomForwarding(args: {
|
||||
browserTabId: string
|
||||
guest: Electron.WebContents
|
||||
resolveRenderer: ResolveRenderer
|
||||
}): () => void {
|
||||
const { browserTabId, guest, resolveRenderer } = args
|
||||
const handler = (event: Electron.Event, mouse: Electron.MouseInputEvent): void => {
|
||||
const direction = resolveGuestMouseWheelZoomDirection(mouse)
|
||||
if (!direction) {
|
||||
return
|
||||
}
|
||||
// Why: wheel input over a focused webview does not reach renderer DOM
|
||||
// handlers, so consume it here and forward to the existing page-zoom path.
|
||||
event.preventDefault()
|
||||
resolveRenderer(browserTabId)?.send('ui:zoomBrowserPage', direction)
|
||||
}
|
||||
|
||||
guest.on('before-mouse-event', handler)
|
||||
return () => {
|
||||
try {
|
||||
guest.off('before-mouse-event', handler)
|
||||
} catch {
|
||||
// Why: best-effort — guest may already be destroyed during teardown.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRendererWebContents(
|
||||
rendererWebContentsIdByTabId: ReadonlyMap<string, number>,
|
||||
browserTabId: string
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
resolveRendererWebContents,
|
||||
setupGrabShortcutForwarding,
|
||||
setupGuestContextMenu,
|
||||
setupGuestMouseWheelZoomForwarding,
|
||||
setupGuestShortcutForwarding
|
||||
} from './browser-guest-ui'
|
||||
import { ANTI_DETECTION_SCRIPT } from './anti-detection'
|
||||
@@ -184,6 +185,7 @@ export class BrowserManager {
|
||||
private readonly contextMenuCleanupByTabId = new Map<string, () => void>()
|
||||
private readonly grabShortcutCleanupByTabId = new Map<string, () => void>()
|
||||
private readonly shortcutForwardingCleanupByTabId = new Map<string, () => void>()
|
||||
private readonly mouseWheelZoomCleanupByTabId = new Map<string, () => void>()
|
||||
private readonly annotationViewportBridgeOpsByTabId = new Map<string, Promise<unknown>>()
|
||||
private readonly worktreeIdByTabId = new Map<string, string>()
|
||||
private readonly policyAttachedGuestIds = new Set<number>()
|
||||
@@ -751,6 +753,7 @@ export class BrowserManager {
|
||||
this.setupContextMenu(browserTabId, guest)
|
||||
this.setupGrabShortcut(browserTabId, guest)
|
||||
this.setupShortcutForwarding(browserTabId, guest)
|
||||
this.setupMouseWheelZoomForwarding(browserTabId, guest)
|
||||
this.flushPendingLoadFailure(browserTabId, webContentsId)
|
||||
this.flushPendingPermissionEvents(browserTabId, webContentsId)
|
||||
this.flushPendingPopupEvents(browserTabId, webContentsId)
|
||||
@@ -786,6 +789,11 @@ export class BrowserManager {
|
||||
fwdCleanup()
|
||||
this.shortcutForwardingCleanupByTabId.delete(browserTabId)
|
||||
}
|
||||
const mouseWheelZoomCleanup = this.mouseWheelZoomCleanupByTabId.get(browserTabId)
|
||||
if (mouseWheelZoomCleanup) {
|
||||
mouseWheelZoomCleanup()
|
||||
this.mouseWheelZoomCleanupByTabId.delete(browserTabId)
|
||||
}
|
||||
// Why: paused downloads wait for explicit product approval. If the owning
|
||||
// browser tab disappears first, cancel the request so the app does not
|
||||
// retain orphaned download items or write files after context is gone.
|
||||
@@ -834,6 +842,7 @@ export class BrowserManager {
|
||||
this.pendingPermissionEventsByGuestId.clear()
|
||||
this.pendingPopupEventsByGuestId.clear()
|
||||
this.pendingDownloadIdsByGuestId.clear()
|
||||
this.mouseWheelZoomCleanupByTabId.clear()
|
||||
this.annotationViewportBridgeOpsByTabId.clear()
|
||||
}
|
||||
|
||||
@@ -1417,6 +1426,24 @@ export class BrowserManager {
|
||||
)
|
||||
}
|
||||
|
||||
private setupMouseWheelZoomForwarding(browserTabId: string, guest: Electron.WebContents): void {
|
||||
const previousCleanup = this.mouseWheelZoomCleanupByTabId.get(browserTabId)
|
||||
if (previousCleanup) {
|
||||
previousCleanup()
|
||||
this.mouseWheelZoomCleanupByTabId.delete(browserTabId)
|
||||
}
|
||||
|
||||
this.mouseWheelZoomCleanupByTabId.set(
|
||||
browserTabId,
|
||||
setupGuestMouseWheelZoomForwarding({
|
||||
browserTabId,
|
||||
guest,
|
||||
resolveRenderer: (tabId) =>
|
||||
resolveRendererWebContents(this.rendererWebContentsIdByTabId, tabId)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private forwardOrQueueGuestLoadFailure(
|
||||
guestWebContentsId: number,
|
||||
loadError: { code: number; description: string; validatedUrl: string }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const AUTO_GRANTED_BROWSER_PERMISSIONS = new Set([
|
||||
'fullscreen',
|
||||
// Agent-browser clipboard commands execute via CDP in this session; denying
|
||||
// them breaks trusted runtime commands even when invoked with a user gesture.
|
||||
'clipboard-read',
|
||||
'clipboard-sanitized-write',
|
||||
// User-opened browser pages need these profile-scoped grants to complete
|
||||
// normal site flows like web push setup and durable app storage.
|
||||
'notifications',
|
||||
// Chromium can request this at runtime even though Electron's TS union does
|
||||
// not list it; chatgpt.com uses it to keep browser storage from eviction.
|
||||
'persistent-storage'
|
||||
])
|
||||
|
||||
export function isAutoGrantedBrowserSessionPermission(permission: string): boolean {
|
||||
return AUTO_GRANTED_BROWSER_PERMISSIONS.has(permission)
|
||||
}
|
||||
@@ -257,23 +257,35 @@ describe('BrowserSessionRegistry persistence', () => {
|
||||
requestHandler(guestWc, 'clipboard-read', permissionCallback)
|
||||
requestHandler(guestWc, 'clipboard-sanitized-write', permissionCallback)
|
||||
requestHandler(guestWc, 'notifications', permissionCallback)
|
||||
requestHandler(guestWc, 'persistent-storage', permissionCallback)
|
||||
requestHandler(guestWc, 'geolocation', permissionCallback)
|
||||
requestHandler(guestWc, 'media', permissionCallback, { mediaTypes: ['video'] })
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(permissionCallback.mock.calls).toEqual([[true], [true], [true], [false], [true]])
|
||||
expect(permissionCallback.mock.calls).toEqual([
|
||||
[true],
|
||||
[true],
|
||||
[true],
|
||||
[true],
|
||||
[true],
|
||||
[false],
|
||||
[true]
|
||||
])
|
||||
)
|
||||
expect(browserManagerNotifyPermissionDeniedMock).toHaveBeenCalledWith({
|
||||
guestWebContentsId: 401,
|
||||
permission: 'notifications',
|
||||
permission: 'geolocation',
|
||||
rawUrl: 'https://example.com/account'
|
||||
})
|
||||
expect(
|
||||
browserManagerNotifyPermissionDeniedMock.mock.calls.map(([args]) => args.permission)
|
||||
).toEqual(['notifications'])
|
||||
).toEqual(['geolocation'])
|
||||
expect(checkHandler(null, 'fullscreen', '')).toBe(true)
|
||||
expect(checkHandler(null, 'clipboard-read', '')).toBe(true)
|
||||
expect(checkHandler(null, 'clipboard-sanitized-write', '')).toBe(true)
|
||||
expect(checkHandler(null, 'notifications', '')).toBe(false)
|
||||
expect(checkHandler(null, 'notifications', '')).toBe(true)
|
||||
expect(checkHandler(null, 'persistent-storage', '')).toBe(true)
|
||||
expect(checkHandler(null, 'geolocation', '')).toBe(false)
|
||||
expect(checkHandler(null, 'media', '', { mediaType: 'video' })).toBe(true)
|
||||
expect(defaultSession.setDisplayMediaRequestHandler).toHaveBeenCalled()
|
||||
const displayMediaHandler = defaultSession.setDisplayMediaRequestHandler.mock.calls[0][0]
|
||||
|
||||
@@ -206,7 +206,9 @@ describe('BrowserSessionRegistry', () => {
|
||||
await vi.waitFor(() => expect(cb).toHaveBeenCalledWith(true))
|
||||
|
||||
expect(checkHandler(null, 'media', '', { mediaType: 'video' })).toBe(true)
|
||||
expect(checkHandler(null, 'notifications', '', {})).toBe(false)
|
||||
expect(checkHandler(null, 'notifications', '', {})).toBe(true)
|
||||
expect(checkHandler(null, 'persistent-storage', '', {})).toBe(true)
|
||||
expect(checkHandler(null, 'geolocation', '', {})).toBe(false)
|
||||
})
|
||||
|
||||
it('wires WebAuthn device selection for isolated partitions', () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { BrowserSessionProfile, BrowserSessionProfileScope } from '../../sh
|
||||
import { browserManager } from './browser-manager'
|
||||
import { hasSystemMediaAccess, requestSystemMediaAccess } from './browser-media-access'
|
||||
import { cleanElectronUserAgent, setupClientHintsOverride } from './browser-session-ua'
|
||||
import { isAutoGrantedBrowserSessionPermission } from './browser-session-permission-policy'
|
||||
import {
|
||||
allowsBrowserWebAuthnPermission,
|
||||
clearBrowserWebAuthnAccessHandlers,
|
||||
@@ -493,10 +494,6 @@ class BrowserSessionRegistry {
|
||||
sess.setUserAgent(cleanUA)
|
||||
setupClientHintsOverride(sess, cleanUA)
|
||||
}
|
||||
// Why: agent-browser clipboard commands execute via CDP in this session.
|
||||
// Until there is a separate trusted bridge, denying clipboard-read breaks
|
||||
// those runtime commands even when invoked with a user gesture.
|
||||
const autoGranted = new Set(['fullscreen', 'clipboard-read', 'clipboard-sanitized-write'])
|
||||
sess.setPermissionRequestHandler((webContents, permission, callback, details) => {
|
||||
// Why: `media` (camera/mic) must defer to macOS TCC instead of being
|
||||
// denied outright. Denying at the session layer would make pages inside
|
||||
@@ -530,7 +527,7 @@ class BrowserSessionRegistry {
|
||||
)
|
||||
return
|
||||
}
|
||||
const allowed = autoGranted.has(permission)
|
||||
const allowed = isAutoGrantedBrowserSessionPermission(permission)
|
||||
if (!allowed) {
|
||||
browserManager.notifyPermissionDenied({
|
||||
guestWebContentsId: webContents.id,
|
||||
@@ -547,7 +544,7 @@ class BrowserSessionRegistry {
|
||||
if (allowsBrowserWebAuthnPermission(permission, details)) {
|
||||
return true
|
||||
}
|
||||
return autoGranted.has(permission)
|
||||
return isAutoGrantedBrowserSessionPermission(permission)
|
||||
})
|
||||
installBrowserWebAuthnAccessHandlers(sess)
|
||||
sess.setDisplayMediaRequestHandler((_request, callback) => {
|
||||
|
||||
Reference in New Issue
Block a user