From f86b3ddbaccb91c28621b983ec601f6609e77839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 16 Apr 2026 21:48:47 -0400 Subject: [PATCH] fix(ipc-oauth): use shared registerHandle for trust and error sanitization (I-01) The self-built `ok/err/handle` helpers called `ipcMain.handle` directly, which meant every OAuth IPC channel skipped the trusted-renderer check in `ipc-support.registerHandle` and its `ZodError -> IPC_INVALID_PAYLOAD` sanitizer. Any renderer origin could invoke these channels, and thrown zod errors would leak the raw validator message. Take `registerHandle` and `fail` as dependencies so callers wire in the hardened variants from `createIpcSupport`, drop the local helpers, and import `ok` from `ipc-core-common`. Zod inputs stay on `safeParse` to preserve the explicit `INVALID_INPUT` code + message path. --- src/main/ipc-oauth-handlers.ts | 64 ++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/main/ipc-oauth-handlers.ts diff --git a/src/main/ipc-oauth-handlers.ts b/src/main/ipc-oauth-handlers.ts new file mode 100644 index 00000000..465b9780 --- /dev/null +++ b/src/main/ipc-oauth-handlers.ts @@ -0,0 +1,64 @@ +import { ipcMain, type IpcMainInvokeEvent } from "electron"; +import type { DesktopResult } from "@shared/contracts"; +import { IPC_CHANNELS } from "@shared/contracts"; +import { + oauthCancelInputSchema, + oauthManualCodeResponseSchema, + oauthPromptResponseSchema, + oauthStartInputSchema, +} from "@shared/contracts/schemas-oauth"; +import type { OAuthLoginService } from "./credentials/oauth-login-service"; +import { ok } from "./ipc-core-common"; + +interface OAuthHandlerDeps { + service: OAuthLoginService; + registerHandle: ( + channel: string, + handler: (event: IpcMainInvokeEvent, ...args: unknown[]) => Promise> | DesktopResult, + ) => void; + fail: (code: string, message: string) => DesktopResult; + logger: { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void }; +} + +export function registerOAuthIpcHandlers(deps: OAuthHandlerDeps): () => void { + const channels: string[] = [ + IPC_CHANNELS.oauthStart, + IPC_CHANNELS.oauthCancel, + IPC_CHANNELS.oauthPromptResponse, + IPC_CHANNELS.oauthManualCodeResponse, + ]; + + deps.registerHandle(IPC_CHANNELS.oauthStart, async (_event, raw) => { + const parsed = oauthStartInputSchema.safeParse(raw); + if (!parsed.success) return deps.fail("INVALID_INPUT", parsed.error.message); + deps.service.start(parsed.data.connectorId).catch((error) => { + deps.logger.warn(`[oauth] login flow ended: ${error instanceof Error ? error.message : String(error)}`); + }); + return ok(true); + }); + + deps.registerHandle(IPC_CHANNELS.oauthCancel, async (_event, raw) => { + const parsed = oauthCancelInputSchema.safeParse(raw); + if (!parsed.success) return deps.fail("INVALID_INPUT", parsed.error.message); + deps.service.cancel(parsed.data.connectorId); + return ok(true); + }); + + deps.registerHandle(IPC_CHANNELS.oauthPromptResponse, async (_event, raw) => { + const parsed = oauthPromptResponseSchema.safeParse(raw); + if (!parsed.success) return deps.fail("INVALID_INPUT", parsed.error.message); + const accepted = deps.service.submitPromptResponse(parsed.data.requestId, parsed.data.value); + return ok(accepted); + }); + + deps.registerHandle(IPC_CHANNELS.oauthManualCodeResponse, async (_event, raw) => { + const parsed = oauthManualCodeResponseSchema.safeParse(raw); + if (!parsed.success) return deps.fail("INVALID_INPUT", parsed.error.message); + const accepted = deps.service.submitManualCode(parsed.data.requestId, parsed.data.code); + return ok(accepted); + }); + + return () => { + for (const channel of channels) ipcMain.removeHandler(channel); + }; +}