diff --git a/apps/desktop/src/preview/BrowserSession.test.ts b/apps/desktop/src/preview/BrowserSession.test.ts index e258bb2dfc5..17fa48958a7 100644 --- a/apps/desktop/src/preview/BrowserSession.test.ts +++ b/apps/desktop/src/preview/BrowserSession.test.ts @@ -14,6 +14,7 @@ const { fromPartition, sessions } = vi.hoisted(() => ({ readonly clearCache: ReturnType; readonly clearStorageData: ReturnType; readonly getUserAgent: ReturnType; + readonly setPermissionCheckHandler: ReturnType; readonly setPermissionRequestHandler: ReturnType; readonly setUserAgent: ReturnType; } @@ -28,6 +29,13 @@ vi.mock("electron", () => ({ import * as BrowserSession from "./BrowserSession.ts"; +type PermissionRequestHandler = ( + webContents: unknown, + permission: string, + callback: (granted: boolean) => void, +) => void; +type PermissionCheckHandler = (webContents: unknown, permission: string) => boolean; + const layer = BrowserSession.layer.pipe(Layer.provide(NodeServices.layer)); describe("BrowserSession", () => { @@ -39,6 +47,7 @@ describe("BrowserSession", () => { clearCache: vi.fn(() => Promise.resolve()), clearStorageData: vi.fn(() => Promise.resolve()), getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/0.0.27"), + setPermissionCheckHandler: vi.fn(), setPermissionRequestHandler: vi.fn(), setUserAgent: vi.fn(), }; @@ -61,6 +70,35 @@ describe("BrowserSession", () => { }).pipe(Effect.provide(layer)), ); + it.effect("grants clipboard permissions for previewed pages", () => + Effect.gen(function* () { + const browserSessions = yield* BrowserSession.BrowserSession; + + const partition = yield* browserSessions.getPartition("scope-a"); + yield* browserSessions.getSession("scope-a"); + const mockSession = sessions.get(partition); + assert.isDefined(mockSession); + + assert.strictEqual(mockSession.setPermissionRequestHandler.mock.calls.length, 1); + const requestHandler = mockSession.setPermissionRequestHandler.mock + .calls[0]?.[0] as PermissionRequestHandler; + const requested = new Map(); + for (const permission of ["clipboard-read", "clipboard-sanitized-write", "midi"]) { + requestHandler(undefined, permission, (granted) => requested.set(permission, granted)); + } + assert.strictEqual(requested.get("clipboard-read"), true); + assert.strictEqual(requested.get("clipboard-sanitized-write"), true); + assert.strictEqual(requested.get("midi"), false); + + assert.strictEqual(mockSession.setPermissionCheckHandler.mock.calls.length, 1); + const checkHandler = mockSession.setPermissionCheckHandler.mock + .calls[0]?.[0] as PermissionCheckHandler; + assert.strictEqual(checkHandler(undefined, "clipboard-read"), true); + assert.strictEqual(checkHandler(undefined, "clipboard-sanitized-write"), true); + assert.strictEqual(checkHandler(undefined, "midi"), false); + }).pipe(Effect.provide(layer)), + ); + it.effect("preserves partition scope and the platform failure chain", () => { const nativeCause = new Error("native digest failed"); const platformCause = PlatformError.systemError({ diff --git a/apps/desktop/src/preview/BrowserSession.ts b/apps/desktop/src/preview/BrowserSession.ts index afa8dafe976..0a35800ce89 100644 --- a/apps/desktop/src/preview/BrowserSession.ts +++ b/apps/desktop/src/preview/BrowserSession.ts @@ -11,6 +11,19 @@ import * as SynchronizedRef from "effect/SynchronizedRef"; const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-"; +const ALLOWED_PERMISSIONS = [ + "clipboard-read", + "clipboard-sanitized-write", + "notifications", + "geolocation", +] as const; +const PERMISSION_CHECK_ALLOWLIST: ReadonlySet = new Set(ALLOWED_PERMISSIONS); +// The request handler can also receive the legacy "clipboard-write" permission name. +const PERMISSION_REQUEST_ALLOWLIST: ReadonlySet = new Set([ + ...ALLOWED_PERMISSIONS, + "clipboard-write", +]); + export class BrowserSessionPartitionDerivationError extends Schema.TaggedErrorClass()( "BrowserSessionPartitionDerivationError", { @@ -120,9 +133,11 @@ export const make = Effect.gen(function* BrowserSessionMake() { .replace(/\s*t3code\/[\d.]+/, ""); browserSession.setUserAgent(userAgent); browserSession.setPermissionRequestHandler((_webContents, permission, callback) => { - const allowed = ["clipboard-read", "clipboard-write", "notifications", "geolocation"]; - callback(allowed.includes(permission)); + callback(PERMISSION_REQUEST_ALLOWLIST.has(permission)); }); + browserSession.setPermissionCheckHandler((_webContents, permission) => + PERMISSION_CHECK_ALLOWLIST.has(permission), + ); const next = new Map(sessions); next.set(partition, browserSession); return [browserSession, next] as const;