diff --git a/apps/desktop/src/preview/BrowserSession.test.ts b/apps/desktop/src/preview/BrowserSession.test.ts index aaf34c3578f9e51c5773122f5f362bbed7cb3d8a..45000736c558e9834c2920c5a8df0c47b570d367 100644 --- a/apps/desktop/src/preview/BrowserSession.test.ts +++ b/apps/desktop/src/preview/BrowserSession.test.ts @@ -13,7 +13,7 @@ const { fromPartition, sessions } = vi.hoisted(() => ({ { readonly clearCache: ReturnType; readonly clearStorageData: ReturnType; - readonly getUserAgent: ReturnType; + readonly getUserAgent: ReturnType string>>; readonly setPermissionRequestHandler: ReturnType; readonly setPermissionCheckHandler: ReturnType; readonly setUserAgent: ReturnType; @@ -102,6 +102,44 @@ describe("BrowserSession", () => { }).pipe(Effect.provide(layer)), ); + // A rewritten session UA — any variant, even ones that keep the Electron + // token — makes Cloudflare Turnstile loop with error 600010 (#5002), so + // the guest must end up with Electron's native User-Agent. The mock applies + // setUserAgent calls, so this fails on any reintroduced rewrite while still + // permitting a harmless re-set of the unchanged native string. + it.effect("keeps the guest's effective User-Agent equal to Electron's native one", () => + Effect.gen(function* () { + // Electron's real UA shape: app token, then Chrome, then Electron, then + // Safari — the token order and casing matter to any strip regex. + const nativeUserAgent = + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) T3Code(Alpha)/0.0.33 Chrome/146.0.7680.216 Electron/41.5.0 Safari/537.36"; + fromPartition.mockReset(); + fromPartition.mockImplementation((partition: string) => { + let userAgent = nativeUserAgent; + const browserSession = { + clearCache: vi.fn(() => Promise.resolve()), + clearStorageData: vi.fn(() => Promise.resolve()), + getUserAgent: vi.fn(() => userAgent), + setPermissionRequestHandler: vi.fn(), + setPermissionCheckHandler: vi.fn(), + setUserAgent: vi.fn((next: string) => { + userAgent = next; + }), + }; + sessions.set(partition, browserSession); + return browserSession; + }); + + const browserSessions = yield* BrowserSession.BrowserSession; + const partition = yield* browserSessions.getPartition("scope-a"); + yield* browserSessions.getSession("scope-a"); + + const browserSession = sessions.get(partition); + assert.isDefined(browserSession); + assert.strictEqual(browserSession.getUserAgent(), nativeUserAgent); + }).pipe(Effect.provide(layer)), + ); + it.effect("grants clipboard-sanitized-write through both the request and check handlers", () => Effect.gen(function* () { const browserSessions = yield* BrowserSession.BrowserSession; diff --git a/apps/desktop/src/preview/BrowserSession.ts b/apps/desktop/src/preview/BrowserSession.ts index 13ec5682e582aba0a627fc82c126056f2457b196..930c13990d51a45b4f6850cc6752bd5947f82cc9 100644 --- a/apps/desktop/src/preview/BrowserSession.ts +++ b/apps/desktop/src/preview/BrowserSession.ts @@ -197,11 +197,12 @@ export const make = Effect.gen(function* BrowserSessionMake() { return Effect.try({ try: () => { const browserSession = session.fromPartition(partition); - const userAgent = browserSession - .getUserAgent() - .replace(/Electron\/[\d.]+ /, "") - .replace(/\s*t3code\/[\d.]+/, ""); - browserSession.setUserAgent(userAgent); + // The guest keeps Electron's native User-Agent. Rewriting it in any + // form — even variants that keep the Electron token — makes Cloudflare + // Turnstile fail its integrity check with error 600010 and recreate + // the challenge every few seconds, so logins behind it never complete + // (#5002). Re-setting the unchanged native string is harmless, so it + // is the rewritten string itself that trips the check. browserSession.setPermissionRequestHandler((_webContents, permission, callback) => { callback(ALLOWED_PREVIEW_PERMISSIONS.has(permission)); });