Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/desktop/src/preview/BrowserSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const { fromPartition, sessions } = vi.hoisted(() => ({
readonly setPermissionRequestHandler: ReturnType<typeof vi.fn>;
readonly setPermissionCheckHandler: ReturnType<typeof vi.fn>;
readonly setUserAgent: ReturnType<typeof vi.fn>;
// SCIENT-FORK:START — Scient Browser: site identity hook.
readonly webRequest: { readonly onBeforeSendHeaders: ReturnType<typeof vi.fn> };
// SCIENT-FORK:END
}
>(),
}));
Expand Down Expand Up @@ -63,6 +66,9 @@ describe("BrowserSession", () => {
setPermissionRequestHandler: vi.fn(),
setPermissionCheckHandler: vi.fn(),
setUserAgent: vi.fn(),
// SCIENT-FORK:START — Scient Browser: site identity hook.
webRequest: { onBeforeSendHeaders: vi.fn() },
// SCIENT-FORK:END
};
sessions.set(partition, browserSession);
return browserSession;
Expand Down Expand Up @@ -142,6 +148,9 @@ describe("BrowserSession", () => {
setUserAgent: vi.fn((next: string) => {
userAgent = next;
}),
// SCIENT-FORK:START — Scient Browser: site identity hook.
webRequest: { onBeforeSendHeaders: vi.fn() },
// SCIENT-FORK:END
};
sessions.set(partition, browserSession);
return browserSession;
Expand All @@ -157,6 +166,25 @@ describe("BrowserSession", () => {
}).pipe(Effect.provide(layer)),
);

// SCIENT-FORK:START — Scient Browser: site identity hook.
it.effect("adds the site identity rule to every browser session", () =>
Effect.gen(function* () {
const browserSessions = yield* BrowserSession.BrowserSession;
const persistent = yield* browserSessions.getPartition("scope-a");
const incognito = yield* browserSessions.getPartition("scope-a", false);
yield* browserSessions.getSession("scope-a");
yield* browserSessions.getSession("scope-a", false);

for (const partition of [persistent, incognito]) {
assert.strictEqual(
sessions.get(partition)?.webRequest.onBeforeSendHeaders.mock.calls.length,
1,
);
}
}).pipe(Effect.provide(layer)),
);
// SCIENT-FORK:END

it.effect("grants preview permissions through both the request and check handlers", () =>
Effect.gen(function* () {
const browserSessions = yield* BrowserSession.BrowserSession;
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/preview/BrowserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import * as ElectronDialog from "../electron/ElectronDialog.ts";
// SCIENT-FORK:START — Scient Accounts: sign-ins outlive the app.
import { keepSessionCookies } from "../scient/accounts/keepSessionCookies.ts";
// SCIENT-FORK:END
// SCIENT-FORK:START — Scient Browser: sites that refuse an app's name open normally.
import { matchSiteBrowserIdentity } from "../scient/browser/siteBrowserIdentity.ts";
// SCIENT-FORK:END

const PREVIEW_PARTITION_PREFIX = SCIENT_DESKTOP_IDENTITY.previewPartitionPrefix;
/** Ephemeral partitions omit persist: but retain a product prefix so the webview gate recognizes them. */
Expand Down Expand Up @@ -292,6 +295,9 @@ export const make = Effect.gen(function* BrowserSessionMake() {
// SCIENT-FORK:START — Scient Accounts: sign-ins outlive the app.
if (persistent) keepSessionCookies(browserSession);
// SCIENT-FORK:END
// SCIENT-FORK:START — Scient Browser: sites that refuse an app's name open normally.
matchSiteBrowserIdentity(browserSession);
// SCIENT-FORK:END
// Like Chrome, clipboard reads ask once per origin; both answers last for the session.
const clipboardReadDecisions = new Map<string, boolean>();
const confirmClipboardRead = (
Expand Down
50 changes: 50 additions & 0 deletions apps/desktop/src/scient/browser/siteBrowserIdentity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it, vi } from "vite-plus/test";

import { matchSiteBrowserIdentity, withoutAppName } from "./siteBrowserIdentity.ts";

const prefix =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)";
const engine = "Chrome/152.0.7977.169 Electron/44.10.0 Safari/537.36";

function listen() {
const onBeforeSendHeaders = vi.fn();
matchSiteBrowserIdentity({ webRequest: { onBeforeSendHeaders } } as never);
const [filter, listener] = onBeforeSendHeaders.mock.calls[0]!;
return { filter, listener };
}

describe("withoutAppName", () => {
it.each(["Scient/0.6.26", "Scient/0.6.27-beta.20261101.1", "ScientDev/44.10.0"])(
"removes %s and keeps Chrome and Electron",
(token) => {
expect(withoutAppName(`${prefix} ${token} ${engine}`)).toBe(`${prefix} ${engine}`);
},
);

it("leaves an identity without an app name alone", () => {
expect(withoutAppName(`${prefix} ${engine}`)).toBe(`${prefix} ${engine}`);
});
});

describe("matchSiteBrowserIdentity", () => {
it("applies to WhatsApp Web only, so other sites keep the native identity", () => {
expect(listen().filter).toEqual({ urls: ["*://web.whatsapp.com/*"] });
});

it("changes the identity and keeps the other headers", () => {
const callback = vi.fn();
listen().listener(
{ requestHeaders: { Accept: "text/html", "User-Agent": `${prefix} Scient/1.0.0 ${engine}` } },
callback,
);
expect(callback).toHaveBeenCalledWith({
requestHeaders: { Accept: "text/html", "User-Agent": `${prefix} ${engine}` },
});
});

it("passes a request without an identity through", () => {
const callback = vi.fn();
listen().listener({ requestHeaders: { Accept: "text/html" } }, callback);
expect(callback).toHaveBeenCalledWith({ requestHeaders: { Accept: "text/html" } });
});
});
28 changes: 28 additions & 0 deletions apps/desktop/src/scient/browser/siteBrowserIdentity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Session } from "electron";

/**
* Sites that refuse any browser naming an app before or after Chrome, even a current one. Keep this
* list short: Cloudflare Turnstile loops on some rewritten identities, so every other site keeps
* Electron's native one. WhatsApp decides on the server, so the request header is enough.
*/
const SITES_REFUSING_APP_NAME = ["*://web.whatsapp.com/*"];

/** Electron's native identity without the `<App>/<version>` token in front of `Chrome/`. */
export function withoutAppName(userAgent: string): string {
return userAgent.replace(/ [^\s/]+\/\S+(?= Chrome\/)/, "");
}

export function matchSiteBrowserIdentity(session: Pick<Session, "webRequest">) {
session.webRequest.onBeforeSendHeaders(
{ urls: SITES_REFUSING_APP_NAME },
({ requestHeaders }, callback) => {
const userAgent = requestHeaders["User-Agent"];
callback({
requestHeaders:
userAgent === undefined
? requestHeaders
: { ...requestHeaders, "User-Agent": withoutAppName(userAgent) },
});
},
);
}
Loading