chore(desktop-client): gate dev-only debug paths in packaged builds

Wrap the network observer, renderer-devtools proxy (both the main-side
ipc handler registration and the preload-side response listener), and
the auto-open-devtools triggers behind !app.isPackaged so packaged
builds don't ship debugging side channels. Add a tiny console-guard
imported first in renderer/main.ts that no-ops console.* in
import.meta.env.PROD so a packaged build stays quiet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 17:20:00 +08:00
parent fb1351d82b
commit 8b4697d605
6 changed files with 117 additions and 58 deletions
@@ -1,3 +1,4 @@
import { app } from "electron/main";
import type { WebContents } from "electron/main"; import type { WebContents } from "electron/main";
function normalizeBooleanEnv(value: string | undefined): boolean | null { function normalizeBooleanEnv(value: string | undefined): boolean | null {
@@ -16,6 +17,10 @@ function normalizeBooleanEnv(value: string | undefined): boolean | null {
} }
export function shouldAutoOpenExecutionDevtools(): boolean { export function shouldAutoOpenExecutionDevtools(): boolean {
if (app.isPackaged) {
return false;
}
const explicit = normalizeBooleanEnv(process.env.GEO_DESKTOP_EXECUTION_DEVTOOLS); const explicit = normalizeBooleanEnv(process.env.GEO_DESKTOP_EXECUTION_DEVTOOLS);
if (explicit !== null) { if (explicit !== null) {
return explicit; return explicit;
@@ -25,6 +30,10 @@ export function shouldAutoOpenExecutionDevtools(): boolean {
} }
export function shouldAutoOpenBackgroundExecutionDevtools(): boolean { export function shouldAutoOpenBackgroundExecutionDevtools(): boolean {
if (app.isPackaged) {
return false;
}
const explicit = normalizeBooleanEnv(process.env.GEO_DESKTOP_EXECUTION_DEVTOOLS); const explicit = normalizeBooleanEnv(process.env.GEO_DESKTOP_EXECUTION_DEVTOOLS);
if (explicit !== null) { if (explicit !== null) {
return explicit; return explicit;
@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { app } from "electron/main";
import type { Session, WebContents } from "electron/main"; import type { Session, WebContents } from "electron/main";
import type { import type {
@@ -39,11 +40,16 @@ const requestFilter = {
urls: ["http://*/*", "https://*/*", "ws://*/*", "wss://*/*"], urls: ["http://*/*", "https://*/*", "ws://*/*", "wss://*/*"],
}; };
const redactedQueryKeys = /token|signature|sig|auth|key|password|session/i; const redactedQueryKeys = /token|signature|sig|auth|key|password|session/i;
const networkObserverEnabled = !app.isPackaged;
let rendererTarget: WebContents | null = null; let rendererTarget: WebContents | null = null;
let originalFetch: typeof globalThis.fetch | null = null; let originalFetch: typeof globalThis.fetch | null = null;
export function installObservedGlobalFetch(): void { export function installObservedGlobalFetch(): void {
if (!networkObserverEnabled) {
return;
}
if (originalFetch || typeof globalThis.fetch !== "function") { if (originalFetch || typeof globalThis.fetch !== "function") {
return; return;
} }
@@ -79,6 +85,10 @@ export function installObservedGlobalFetch(): void {
} }
export function registerObservedRequestRendererTarget(webContents: WebContents): void { export function registerObservedRequestRendererTarget(webContents: WebContents): void {
if (!networkObserverEnabled) {
return;
}
rendererTarget = webContents; rendererTarget = webContents;
webContents.once("destroyed", () => { webContents.once("destroyed", () => {
if (rendererTarget === webContents) { if (rendererTarget === webContents) {
@@ -91,6 +101,10 @@ export function observeSessionRequests(
target: Session, target: Session,
options: { label: string; partition?: string | null }, options: { label: string; partition?: string | null },
): void { ): void {
if (!networkObserverEnabled) {
return;
}
if (observedSessions.has(target)) { if (observedSessions.has(target)) {
return; return;
} }
@@ -175,6 +189,10 @@ export function observeSessionRequests(
} }
export function startObservedRequest(options: StartObservedRequestOptions): string { export function startObservedRequest(options: StartObservedRequestOptions): string {
if (!networkObserverEnabled) {
return "";
}
const id = `main:${randomUUID()}`; const id = `main:${randomUUID()}`;
const active: ActiveObservedRequest = { const active: ActiveObservedRequest = {
id, id,
@@ -239,6 +257,14 @@ export function failObservedRequest(id: string, error: unknown): void {
} }
export function getObservedRequestSnapshot(): DesktopObservedRequestSnapshot { export function getObservedRequestSnapshot(): DesktopObservedRequestSnapshot {
if (!networkObserverEnabled) {
return {
rendererAttached: false,
activeCount: 0,
recent: [],
};
}
return { return {
rendererAttached: Boolean(rendererTarget && !rendererTarget.isDestroyed()), rendererAttached: Boolean(rendererTarget && !rendererTarget.isDestroyed()),
activeCount: activeRequests.size, activeCount: activeRequests.size,
@@ -1,4 +1,4 @@
import { ipcMain } from "electron/main"; import { app, ipcMain } from "electron/main";
import type { IpcMainEvent, WebContents } from "electron/main"; import type { IpcMainEvent, WebContents } from "electron/main";
interface RendererProxyRequest { interface RendererProxyRequest {
@@ -26,10 +26,17 @@ const pendingRequests = new Map<string, {
}>(); }>();
const rendererProxyRequestChannel = "desktop:renderer-devtools-proxy:request"; const rendererProxyRequestChannel = "desktop:renderer-devtools-proxy:request";
const rendererProxyResponseChannel = "desktop:renderer-devtools-proxy:response"; const rendererProxyResponseChannel = "desktop:renderer-devtools-proxy:response";
const rendererDevtoolsProxyEnabled = !app.isPackaged;
ipcMain.on(rendererProxyResponseChannel, handleRendererProxyResponse); if (rendererDevtoolsProxyEnabled) {
ipcMain.on(rendererProxyResponseChannel, handleRendererProxyResponse);
}
export function registerRendererDevtoolsProxyTarget(webContents: WebContents): void { export function registerRendererDevtoolsProxyTarget(webContents: WebContents): void {
if (!rendererDevtoolsProxyEnabled) {
return;
}
rendererWebContents = webContents; rendererWebContents = webContents;
webContents.once("destroyed", () => { webContents.once("destroyed", () => {
if (rendererWebContents === webContents) { if (rendererWebContents === webContents) {
@@ -39,12 +46,17 @@ export function registerRendererDevtoolsProxyTarget(webContents: WebContents): v
} }
export function canUseRendererDevtoolsProxy(): boolean { export function canUseRendererDevtoolsProxy(): boolean {
return Boolean(rendererWebContents && !rendererWebContents.isDestroyed()); return rendererDevtoolsProxyEnabled
&& Boolean(rendererWebContents && !rendererWebContents.isDestroyed());
} }
export async function rendererDevtoolsFetch( export async function rendererDevtoolsFetch(
request: RendererProxyRequest, request: RendererProxyRequest,
): Promise<RendererProxyResponse> { ): Promise<RendererProxyResponse> {
if (!rendererDevtoolsProxyEnabled) {
throw new Error("renderer_devtools_proxy_unavailable");
}
if (!rendererWebContents || rendererWebContents.isDestroyed()) { if (!rendererWebContents || rendererWebContents.isDestroyed()) {
throw new Error("renderer_devtools_proxy_unavailable"); throw new Error("renderer_devtools_proxy_unavailable");
} }
+57 -55
View File
@@ -35,64 +35,66 @@ interface DesktopAppSettings {
const rendererProxyRequestChannel = "desktop:renderer-devtools-proxy:request"; const rendererProxyRequestChannel = "desktop:renderer-devtools-proxy:request";
const rendererProxyResponseChannel = "desktop:renderer-devtools-proxy:response"; const rendererProxyResponseChannel = "desktop:renderer-devtools-proxy:response";
ipcRenderer.on( if (import.meta.env.DEV) {
rendererProxyRequestChannel, ipcRenderer.on(
async ( rendererProxyRequestChannel,
_event, async (
payload: { _event,
requestId?: unknown; payload: {
request?: { requestId?: unknown;
url?: unknown; request?: {
method?: unknown; url?: unknown;
headers?: unknown; method?: unknown;
body?: unknown; headers?: unknown;
}; body?: unknown;
},
) => {
const requestId = typeof payload?.requestId === "string" ? payload.requestId : "";
const request = payload?.request;
if (!requestId || !request || typeof request !== "object") {
return;
}
const url = typeof request.url === "string" ? request.url : "";
const method = typeof request.method === "string" ? request.method : "GET";
const headers = isStringRecord(request.headers) ? request.headers : {};
const body = typeof request.body === "string" ? request.body : undefined;
const response = await (async () => {
try {
const fetchResponse = await fetch(url, {
method,
headers,
body,
});
const bodyText = await fetchResponse.text();
return {
ok: fetchResponse.ok,
status: fetchResponse.status,
statusText: fetchResponse.statusText,
headers: Object.fromEntries(fetchResponse.headers.entries()),
bodyText,
};
} catch (error) {
return {
ok: false,
status: 0,
statusText: "",
headers: {},
bodyText: "",
error: String(error && ((error as Error).message || error)),
}; };
},
) => {
const requestId = typeof payload?.requestId === "string" ? payload.requestId : "";
const request = payload?.request;
if (!requestId || !request || typeof request !== "object") {
return;
} }
})();
ipcRenderer.send(rendererProxyResponseChannel, { const url = typeof request.url === "string" ? request.url : "";
requestId, const method = typeof request.method === "string" ? request.method : "GET";
response, const headers = isStringRecord(request.headers) ? request.headers : {};
}); const body = typeof request.body === "string" ? request.body : undefined;
},
); const response = await (async () => {
try {
const fetchResponse = await fetch(url, {
method,
headers,
body,
});
const bodyText = await fetchResponse.text();
return {
ok: fetchResponse.ok,
status: fetchResponse.status,
statusText: fetchResponse.statusText,
headers: Object.fromEntries(fetchResponse.headers.entries()),
bodyText,
};
} catch (error) {
return {
ok: false,
status: 0,
statusText: "",
headers: {},
bodyText: "",
error: String(error && ((error as Error).message || error)),
};
}
})();
ipcRenderer.send(rendererProxyResponseChannel, {
requestId,
response,
});
},
);
}
const desktopBridge = { const desktopBridge = {
app: { app: {
@@ -0,0 +1,8 @@
if (import.meta.env.PROD) {
const noop = () => undefined;
console.log = noop;
console.info = noop;
console.warn = noop;
console.error = noop;
console.debug = noop;
}
+2
View File
@@ -1,3 +1,5 @@
import "./console-guard";
import { createApp } from "vue"; import { createApp } from "vue";
import App from "./App.vue"; import App from "./App.vue";