mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 12:47:42 +02:00
[eric] browser: detect Electron by the preload bridge not the UA string, so the Electron token can be stripped app-wide and cross-origin sign-in iframes stop reading as an embedded browser (ENG-238)
This commit is contained in:
@@ -507,6 +507,15 @@ app.commandLine.appendSwitch('disable-backgrounding-occluded-windows');
|
||||
// taking the app down with it. Fails quiet: at worst a brief compositor blip.
|
||||
app.commandLine.appendSwitch('disable-gpu-process-crash-limit');
|
||||
|
||||
// A cross-origin sign-in iframe (Google's GSI button) reads navigator.userAgent in its OWN JS, and
|
||||
// an out-of-process frame gets that value from the app-wide fallback, NOT the webview `useragent`
|
||||
// attribute or any session override (both miss OOPIFs, measured). With the Electron token present
|
||||
// Google refuses sign-in as an "embedded browser" (ENG-238). Strip only the Electron token here so
|
||||
// the openswarm product token stays. Safe because the frontend now detects Electron via the
|
||||
// window.openswarm preload bridge, not this string (see frontend isElectron.ts); nothing reads the
|
||||
// UA for "Electron" anymore, and the backend never gated on it.
|
||||
try { app.userAgentFallback = app.userAgentFallback.replace(/\s*Electron\/\S+/i, ''); } catch (_) { /* older Electron */ }
|
||||
|
||||
let mainWindow = null;
|
||||
let backendProcess = null;
|
||||
let backendRespawns = 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { isElectron as detectElectron } from '@/shared/isElectron';
|
||||
import { requestWebviewAttachSlot, releaseWebviewAttachSlot } from './webviewAttachQueue';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { subscribeLiveDrag } from '../hooks/interaction/liveDragChannel';
|
||||
@@ -154,7 +155,7 @@ function markWindowsWebviewSurvived(): void {
|
||||
}
|
||||
|
||||
const isWindows = navigator.userAgent.includes('Windows');
|
||||
const inElectron = navigator.userAgent.includes('Electron');
|
||||
const inElectron = detectElectron();
|
||||
const isElectron = inElectron && (!isWindows || windowsWebviewEnabled());
|
||||
|
||||
// Keep the openswarm/<ver> product token: Google's sign-in flags a BARE Chrome UA as not-genuine-Chrome and blocks it ("browser may not be secure"), but tolerates a UA carrying a product token. Only the Electron token must go (that one Google hard-blocks).
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { isElectron } from '@/shared/isElectron';
|
||||
import Box from '@mui/material/Box';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import Typography from '@mui/material/Typography';
|
||||
@@ -55,7 +56,7 @@ const MIN_H = 200;
|
||||
// has no login/scroll state worth keeping, so we just unmount the webview when the card is off-screen or
|
||||
// too small to read, and remount (reload) on return. Asymmetric: resume instantly, suspend after a beat
|
||||
// so panning past a card doesn't reload it.
|
||||
const isElectron = typeof navigator !== 'undefined' && navigator.userAgent.includes('Electron');
|
||||
const inElectron = isElectron();
|
||||
const APP_PREVIEW_MIN_PX = 260; // below this on-screen width the live page is indistinguishable from a still
|
||||
const APP_PREVIEW_MARGIN_PX = 400; // resume once the card is within this of the viewport
|
||||
const APP_SUSPEND_SETTLE_MS = 1200;
|
||||
@@ -223,7 +224,7 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
want = false; // reveal-parked: never boot until the first click clears the defer
|
||||
} else if (alwaysLive) {
|
||||
// Actively used (selected, interacting, agent-driven, tiled, fullscreen): pinned, never capped.
|
||||
if (isElectron) requestAppSlot(cardKey, 0, true);
|
||||
if (inElectron) requestAppSlot(cardKey, 0, true);
|
||||
want = true;
|
||||
} else {
|
||||
const now = getCanvasState();
|
||||
@@ -241,7 +242,7 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
const vh = vpH / now.zoom + 2 * m;
|
||||
onscreen = cardX < vx + vw && cardX + cardWidth > vx && cardY < vy + vh && cardY + cardHeight > vy;
|
||||
}
|
||||
if (!onscreen || !isElectron) {
|
||||
if (!onscreen || !inElectron) {
|
||||
want = onscreen; // non-Electron previews are cheap iframes, no renderer to cap
|
||||
} else {
|
||||
// On-screen but passive: go live only if the hard cap has a slot; closest-to-center wins it.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { isElectron } from '@/shared/isElectron';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { store } from '@/shared/state/store';
|
||||
import {
|
||||
@@ -15,7 +16,7 @@ import { guestBudgetHasRoom, wireBrowserLiveCounter } from '@/shared/appWebviewB
|
||||
import { useAppHidden } from './useAppHidden';
|
||||
import { cardIntersectsViewport, distFromCenter, type Viewport } from './suspendGeometry';
|
||||
|
||||
const isElectron = typeof navigator !== 'undefined' && navigator.userAgent.includes('Electron');
|
||||
const inElectron = isElectron();
|
||||
|
||||
// Feed the global guest budget the live-browser count, so apps and browsers share ONE ceiling.
|
||||
wireBrowserLiveCounter(() => {
|
||||
@@ -128,12 +129,12 @@ export function useWebviewSuspend(
|
||||
const vpRef = useRef<Viewport>({ panX, panY, zoom, vpW: 1200, vpH: 800 });
|
||||
|
||||
// Hidden long enough = the user left; park every idle renderer, working agents keep theirs.
|
||||
const appHidden = useAppHidden(isElectron);
|
||||
const appHidden = useAppHidden(inElectron);
|
||||
|
||||
// Window resize changes the viewport without touching pan/zoom/cards; tick so the evaluation below reruns, or a shrunken window never suspends anything.
|
||||
const [resizeTick, setResizeTick] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!isElectron) return;
|
||||
if (!inElectron) return;
|
||||
let t: ReturnType<typeof setTimeout> | null = null;
|
||||
const onResize = () => {
|
||||
if (t) clearTimeout(t);
|
||||
@@ -147,7 +148,7 @@ export function useWebviewSuspend(
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron) return;
|
||||
if (!inElectron) return;
|
||||
const el = viewportRef.current;
|
||||
vpRef.current = {
|
||||
panX, panY, zoom,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, useEffect, useMemo, useCallback, forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { isElectron } from '@/shared/isElectron';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Skeleton } from '@/app/components/feedback/Loading';
|
||||
@@ -12,7 +13,7 @@ import RunInDesktopMessage from '@/app/components/RunInDesktopMessage';
|
||||
import { registerWebview, unregisterWebview, setActiveTab, type BrowserWebview } from '@/shared/browserRegistry';
|
||||
|
||||
// In Electron use <webview> to escape iframe restrictions (popups, mic/camera, WebAuthn, cookied fetch); a srcdoc app still uses an iframe (data:text/html breaks webview same-origin); outside Electron we show a launch-correctly message.
|
||||
const isElectron = navigator.userAgent.includes('Electron');
|
||||
const inElectron = isElectron();
|
||||
|
||||
// Card previews render small; downscale + JPEG so thumbnails don't bloat the output JSON or every list fetch.
|
||||
const THUMB_WIDTH = 600;
|
||||
@@ -207,7 +208,7 @@ const ViewPreview = forwardRef<ViewPreviewHandle, Props>(({
|
||||
}, [serveUrl, frontendCode, inputData, backendResult]);
|
||||
|
||||
// Webview only when we have a real serveUrl; data:text/html for srcdoc breaks same-origin in the Electron sandbox.
|
||||
const useWebview = isElectron && !!iframeSrc;
|
||||
const useWebview = inElectron && !!iframeSrc;
|
||||
|
||||
// Webview's contentDocument is null from the host (separate renderer process); element selection skips it (known regression).
|
||||
useEffect(() => {
|
||||
@@ -473,7 +474,7 @@ const ViewPreview = forwardRef<ViewPreviewHandle, Props>(({
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
) : isElectron ? (
|
||||
) : inElectron ? (
|
||||
// In Electron but not useWebview = a srcdoc app (no serveUrl); the iframe is REQUIRED here (data:text/html breaks webview same-origin), not a fallback. Non-Electron falls through to the launch-correctly message.
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Are we running inside the Electron shell? Detect it by the preload bridge, NOT by sniffing
|
||||
// navigator.userAgent for "Electron": the UA token is deliberately stripped in the browser
|
||||
// partition (and can be stripped app-wide) so sign-in walls stop seeing an embedded browser, which
|
||||
// would make a UA-string check silently wrong. window.openswarm is exposed synchronously in preload
|
||||
// before the first frontend frame, so it is a reliable structural signal (ENG-238).
|
||||
export function isElectron(): boolean {
|
||||
return typeof window !== 'undefined' && !!(window as unknown as { openswarm?: unknown }).openswarm;
|
||||
}
|
||||
Reference in New Issue
Block a user