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: 21 additions & 7 deletions packages/app/src/main/codexAppServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { existsSync, statSync } from "node:fs";
import { dirname, join } from "node:path";
import { app } from "electron";
import { discoverExistingCodexPaths } from "./codexNativeDiscovery";
import { logger } from "./utils/logger";
import type {
CodexIncomingMessage,
CodexNotifyParams,
Expand Down Expand Up @@ -164,16 +165,24 @@ export class CodexAppServer {
this.rejectPending(new Error("codex app-server stopped"));
try {
this.rl?.close();
} catch {}
} catch (error) {
logger.warn("codex-server", "failed to close readline", error);
}
try {
proc.stdin?.destroy();
} catch {}
} catch (error) {
logger.warn("codex-server", "failed to destroy stdin", error);
}
try {
proc.stdout?.destroy();
} catch {}
} catch (error) {
logger.warn("codex-server", "failed to destroy stdout", error);
}
try {
proc.stderr?.destroy();
} catch {}
} catch (error) {
logger.warn("codex-server", "failed to destroy stderr", error);
}
try {
if (process.platform === "win32" && pid) {
const killer = spawn("taskkill.exe", ["/pid", String(pid), "/t", "/f"], {
Expand All @@ -185,14 +194,19 @@ export class CodexAppServer {
} else {
proc.kill();
}
} catch {
} catch (error) {
logger.warn("codex-server", "primary kill failed, retrying", error);
try {
proc.kill();
} catch {}
} catch (retryError) {
logger.warn("codex-server", "retry kill also failed", retryError);
}
}
try {
proc.unref();
} catch {}
} catch (error) {
logger.warn("codex-server", "failed to unref process", error);
}
this.proc = undefined;
this.rl = undefined;
}
Expand Down
24 changes: 17 additions & 7 deletions packages/app/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { app, BrowserWindow, Menu } from "electron";
import { readFile, rm, stat } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { installContentSecurityPolicy } from "./security/contentSecurityPolicy";
import { logger } from "./utils/logger";
import {
IPC_APP_CHANNELS,
IPC_EVENT_CHANNELS,
Expand Down Expand Up @@ -66,7 +68,9 @@ function sendToRenderer(channel: string, payload: unknown) {
if (!mainWindow || mainWindow.isDestroyed()) return;
try {
mainWindow.webContents.send(channel, payload);
} catch {}
} catch (error) {
logger.warn("ipc", `sendToRenderer failed on channel '${channel}'`, error);
}
}

const updateService = new UpdateService((payload) => {
Expand Down Expand Up @@ -107,12 +111,12 @@ function stopCodexServersForClose(_reason: string) {
try {
codexServerManager.stopAll();
} catch (error) {
console.warn("[app-close] stop codex servers failed", error);
logger.warn("app-close", "stop codex servers failed", error);
}
try {
deepSeekResponsesProxyService.stop();
} catch (error) {
console.warn("[app-close] stop DeepSeek proxy failed", error);
logger.warn("app-close", "stop DeepSeek proxy failed", error);
}
}

Expand All @@ -125,7 +129,7 @@ function clearAppCloseForceExitWatchdog() {
function armAppCloseForceExitWatchdog() {
clearAppCloseForceExitWatchdog();
appCloseForceExitTimer = setTimeout(() => {
console.warn("[app-close] force exiting after close watchdog timeout");
logger.warn("app-close", "force exiting after close watchdog timeout");
stopCodexServersForClose("force-exit-watchdog");
app.exit(0);
}, APP_CLOSE_FORCE_EXIT_MS);
Expand Down Expand Up @@ -158,7 +162,7 @@ async function runAppCloseFlow(win: BrowserWindow): Promise<void> {
if (!win.isDestroyed()) win.close();
})()
.catch((error) => {
console.error("[app-close] flow failed", error);
logger.error("app-close", "flow failed", error);
allowMainWindowClose = true;
if (!win.isDestroyed()) win.close();
})
Expand Down Expand Up @@ -189,6 +193,10 @@ app
Menu.setApplicationMenu(null);
}

if (!isDev) {
installContentSecurityPolicy();
}

const historyCachePath = join(app.getPath("userData"), "thread-history-cache.json");
const historyStore = new HistoryStore(historyCachePath);
const historyService = new HistoryService(historyStore);
Expand Down Expand Up @@ -231,7 +239,9 @@ app
const raw = await readFile(historyCachePath, "utf8");
const parsed = JSON.parse(raw);
items = Array.isArray(parsed?.items) ? parsed.items.length : 0;
} catch {}
} catch (error) {
logger.warn("cache", "failed to read history cache stats", error);
}
return {
items,
bytes,
Expand Down Expand Up @@ -317,6 +327,6 @@ app
});
})
.catch((error) => {
console.error("[main] app bootstrap failed", error);
logger.error("main", "app bootstrap failed", error);
app.exit(1);
});
30 changes: 30 additions & 0 deletions packages/app/src/main/security/contentSecurityPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { session } from "electron";

/**
* 生产环境 CSP 策略:
* - 禁止 eval 和外部脚本注入
* - 允许内联样式(Vue/Tailwind 需要)和内联脚本(index.html 主题初始化)
* - 图片允许 data: URI 和 HTTPS 源
* - 网络请求限制为 HTTPS 和本地开发地址
*/
const CSP_DIRECTIVES = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https: http://localhost:* ws://localhost:*",
"font-src 'self' data:",
"object-src 'none'",
"base-uri 'self'",
].join("; ");

export function installContentSecurityPolicy(): void {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
"Content-Security-Policy": [CSP_DIRECTIVES],
},
});
});
}
4 changes: 3 additions & 1 deletion packages/app/src/main/services/LocalSettingsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type UserLocalSettingsPatch,
} from "../../common/localSettings";
import { SecureStorageService } from "./SecureStorageService";
import { logger } from "../utils/logger";

function tryParseJson(text: string): unknown {
try {
Expand Down Expand Up @@ -62,7 +63,8 @@ export class LocalSettingsService {
const raw = await readFile(this.filePath, "utf8");
const settings = normalizeUserLocalSettings(tryParseJson(raw));
return { exists: true, settings: this.decryptApiKeys(settings) };
} catch {
} catch (error) {
logger.info("settings", `settings file not available, using defaults (${this.filePath})`);
return { exists: false, settings: normalizeUserLocalSettings(DEFAULT_USER_LOCAL_SETTINGS) };
}
}
Expand Down
19 changes: 13 additions & 6 deletions packages/app/src/main/services/SecureStorageService.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { safeStorage } from "electron";
import { logger } from "../utils/logger";

const ENCRYPTED_PREFIX = "enc:";

/**
* Wraps Electron's safeStorage (DPAPI on Windows) to encrypt/decrypt
* sensitive strings before persisting them to disk.
*
* Encrypted values are stored as "enc:<base64>" so the reader can
* distinguish them from legacy plaintext values and migrate gracefully.
* Encrypted values are stored as "enc:<base64>".
* Unrecognized values (no prefix) are treated as invalid and discarded.
*/
export class SecureStorageService {
isAvailable(): boolean {
Expand All @@ -16,18 +17,24 @@ export class SecureStorageService {

encrypt(plaintext: string | null): string | null {
if (plaintext == null || plaintext === "") return plaintext;
if (!this.isAvailable()) return plaintext;
if (!this.isAvailable()) {
logger.warn("secure-storage", "encryption unavailable, value will not be persisted securely");
return null;
}
const encrypted = safeStorage.encryptString(plaintext);
return `${ENCRYPTED_PREFIX}${encrypted.toString("base64")}`;
}

decrypt(stored: string | null): string | null {
if (stored == null || stored === "") return stored;
if (!stored.startsWith(ENCRYPTED_PREFIX)) {
// Legacy plaintext value — return as-is for backward compatibility.
return stored;
logger.warn("secure-storage", "discarding unencrypted value — re-enter the key in settings");
return null;
}
if (!this.isAvailable()) {
logger.warn("secure-storage", "decryption unavailable, cannot read encrypted value");
return null;
}
if (!this.isAvailable()) return stored;
const base64 = stored.slice(ENCRYPTED_PREFIX.length);
const buffer = Buffer.from(base64, "base64");
return safeStorage.decryptString(buffer);
Expand Down
31 changes: 31 additions & 0 deletions packages/app/src/main/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 主进程统一日志工具。
*
* 替代散落的 console.warn/console.error 和空 catch,为后续接入文件日志或上报预留统一入口。
*/

type LogLevel = "debug" | "info" | "warn" | "error";

function formatTag(tag: string): string {
return `[${tag}]`;
}

function log(level: LogLevel, tag: string, message: string, error?: unknown): void {
const prefix = formatTag(tag);
if (level === "error") {
console.error(prefix, message, error ?? "");
} else if (level === "warn") {
console.warn(prefix, message, error ?? "");
} else if (level === "info") {
console.info(prefix, message);
} else {
console.debug(prefix, message);
}
}

export const logger = {
debug: (tag: string, message: string) => log("debug", tag, message),
info: (tag: string, message: string) => log("info", tag, message),
warn: (tag: string, message: string, error?: unknown) => log("warn", tag, message, error),
error: (tag: string, message: string, error?: unknown) => log("error", tag, message, error),
};
13 changes: 10 additions & 3 deletions packages/app/src/main/windows/mainWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { app, BrowserWindow, shell } from "electron";
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { normalizeSafeExternalUrl } from "../utils/externalUrl";
import { logger } from "../utils/logger";
import { IPC_APP_CHANNELS } from "@codenexus/shared/ipc/channels";
import type { AppWindowState } from "@codenexus/shared/ipc/contracts";
import { resolveUiFontSizeZoomFactor, type UserLocalSettings } from "@codenexus/shared/localSettings";
Expand Down Expand Up @@ -64,7 +65,9 @@ export async function createMainWindow(opts: MainWindowOptions): Promise<Browser
if (Number.isFinite(zoomFactor) && zoomFactor > 0) {
win.webContents.setZoomFactor(zoomFactor);
}
} catch {}
} catch (error) {
logger.warn("window", "failed to set initial zoom factor", error);
}

// 禁止渲染进程自行打开新窗口;外链统一交给系统浏览器。
win.webContents.setWindowOpenHandler(({ url }) => {
Expand All @@ -84,7 +87,9 @@ export async function createMainWindow(opts: MainWindowOptions): Promise<Browser
try {
win.setMenuBarVisibility(false);
win.removeMenu();
} catch {}
} catch (error) {
logger.warn("window", "failed to hide menu bar", error);
}

const toWindowState = (): AppWindowState => ({
isMaximized: win.isMaximized(),
Expand All @@ -95,7 +100,9 @@ export async function createMainWindow(opts: MainWindowOptions): Promise<Browser
const pushWindowState = () => {
try {
win.webContents.send(IPC_APP_CHANNELS.appWindowState, toWindowState());
} catch {}
} catch (error) {
logger.warn("window", "failed to push window state", error);
}
};

// 首屏加载完成后同步一次状态;并在窗口状态变化时持续推送。
Expand Down
Loading