From 9d38ec20f183c12c7530312b1bf21543ebd84332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 22 Apr 2026 19:07:08 -0400 Subject: [PATCH] fix(ipc): expose classified handler codes --- src/main/ipc-support.ts | 12 +-- tests/unit/ipc-support-error-code.test.ts | 89 +++++++++++++++++++++++ 2 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 tests/unit/ipc-support-error-code.test.ts diff --git a/src/main/ipc-support.ts b/src/main/ipc-support.ts index 9c29a4bb..9edef637 100644 --- a/src/main/ipc-support.ts +++ b/src/main/ipc-support.ts @@ -44,16 +44,16 @@ export function createIpcSupport({ getDb, notificationService, }: IpcSupportDependencies) { - function sanitizeHandlerError(error: unknown) { - if (error instanceof ZodError) { + function sanitizeHandlerError(classified: { code: string }) { + if (classified.code === "ipc.payload.invalid") { return { - code: "IPC_INVALID_PAYLOAD", + code: classified.code, message: "The desktop request payload is invalid.", }; } return { - code: "IPC_HANDLER_FAILED", - message: "The desktop operation failed. Review the application logs for details.", + code: classified.code, + message: "The desktop operation failed. Use the error code when reporting this issue.", }; } @@ -81,7 +81,7 @@ export function createIpcSupport({ context: { channel }, }); const rawMessage = error instanceof Error ? error.message : String(error); - const sanitized = sanitizeHandlerError(error); + const sanitized = sanitizeHandlerError(classified); logger.error(`[${classified.code}] [ipc:${channel}] ${rawMessage}`); return fail(sanitized.code, sanitized.message); } diff --git a/tests/unit/ipc-support-error-code.test.ts b/tests/unit/ipc-support-error-code.test.ts new file mode 100644 index 00000000..300bcf31 --- /dev/null +++ b/tests/unit/ipc-support-error-code.test.ts @@ -0,0 +1,89 @@ +import type { IpcMainInvokeEvent } from "electron"; +import { z } from "zod"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { DesktopResult } from "@shared/contracts"; + +const { ipcHandlers, ipcHandle } = vi.hoisted(() => { + const handlers = new Map Promise>>(); + return { + ipcHandlers: handlers, + ipcHandle: vi.fn((channel: string, handler: (event: IpcMainInvokeEvent, ...args: unknown[]) => Promise>) => { + handlers.set(channel, handler); + }), + }; +}); + +vi.mock("electron", () => ({ + ipcMain: { + handle: ipcHandle, + }, + Notification: { + isSupported: () => false, + }, +})); + +import { createIpcSupport } from "../../src/main/ipc-support"; + +describe("IPC support error codes", () => { + beforeEach(() => { + ipcHandlers.clear(); + ipcHandle.mockClear(); + }); + + it("returns the classified handler code without leaking the raw exception message", async () => { + const { registerHandle } = createSupport(); + registerHandle("test:throws", () => { + throw new Error("database password leaked in raw error"); + }); + + const result = await invokeRegisteredHandler("test:throws"); + + expect(result).toEqual({ + ok: false, + error: { + code: "ipc.handler.failed", + message: "The desktop operation failed. Use the error code when reporting this issue.", + }, + }); + }); + + it("returns the classified payload code for zod validation failures", async () => { + const { registerHandle } = createSupport(); + registerHandle("test:payload", () => { + z.object({ name: z.string() }).parse({ name: 42 }); + return { ok: true, data: true }; + }); + + const result = await invokeRegisteredHandler("test:payload"); + + expect(result).toEqual({ + ok: false, + error: { + code: "ipc.payload.invalid", + message: "The desktop request payload is invalid.", + }, + }); + }); +}); + +function createSupport() { + return createIpcSupport({ + isDev: true, + devServerOrigin: "http://localhost:5173", + logger: { error: vi.fn() }, + getMainWindow: () => null, + getDb: () => ({ + hasCompany: () => true, + belongsToCompany: () => true, + }), + }); +} + +async function invokeRegisteredHandler(channel: string): Promise> { + const handler = ipcHandlers.get(channel); + if (!handler) throw new Error(`No IPC handler registered for ${channel}.`); + return handler({ + senderFrame: { url: "http://localhost:5173/app" }, + sender: { getURL: () => "http://localhost:5173/app" }, + } as IpcMainInvokeEvent); +}