From 8cee0cfd4c05fb1b3b83308604b45376158edde0 Mon Sep 17 00:00:00 2001 From: Matecore Date: Mon, 29 Jun 2026 15:42:53 -0300 Subject: [PATCH 1/3] Fix scanner stdout streaming Signed-off-by: Matecore --- README.md | 14 +++++++ src/main/indexer/scanner.test.ts | 47 ++++++++++++++++++---- src/main/indexer/scanner.ts | 69 ++++++++++++++++++++++++++------ 3 files changed, 110 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e4a4dd7..7ac788c 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,12 @@ vp run dev ## Modes / Modos +## Product Boundaries / Límites del producto + +Code Club IDE is intentionally split into three bounded modes, not one mixed surface. Coding Mode is the core IDE, Studio Mode is a local table-based project workspace, and Design Mode is a lightweight vector design tool. Each mode has its own data model, workflow, and responsibility. + +Code Club IDE está dividido intencionalmente en tres modos con límites claros, no en una sola superficie mezclada. Modo Código es el IDE principal, Modo Studio es un espacio local de gestión basado en tablas, y Modo Diseño es una herramienta liviana de diseño vectorial. Cada modo tiene su propio modelo de datos, flujo de trabajo y responsabilidad. + **Coding Mode / Modo Código** Standard IDE. File explorer, Monaco editor (same engine as VS Code), integrated terminal (PowerShell, WSL, Git Bash), AI agent panel. Multi-chat sessions, sandbox safety mode, checkpoints with rollback, split layouts (single, 2-col, 4-quadrant). @@ -98,6 +104,14 @@ Todo el tráfico de IA va directo de tu dispositivo al proveedor. codeclub no pr | 7 | The debugging takes as long as it has to take. | El debugging durará lo que tenga que durar. | | 8 | First night at Code Club? Open the editor and tame the silicon beast. | ¿Primera noche en el Code Club? Abrís el editor y domás a la bestia de silicio. | +## Contributors / Colaboradores + +Thanks to everyone helping build Code Club IDE. / Gracias a todas las personas que ayudan a construir Code Club IDE. + + + Contributors / Colaboradores + + ## Licensing / Licencia | License / Licencia | Use Case / Caso de Uso | Cost / Costo | diff --git a/src/main/indexer/scanner.test.ts b/src/main/indexer/scanner.test.ts index c8767aa..ec3c1ed 100644 --- a/src/main/indexer/scanner.test.ts +++ b/src/main/indexer/scanner.test.ts @@ -1,40 +1,73 @@ +import { EventEmitter } from "events"; import { beforeEach, describe, expect, it } from "vite-plus/test"; import { vi } from "vitest"; const execFile = vi.fn(); +const spawn = vi.fn(); const existsSync = vi.fn(); vi.mock("electron", () => ({ app: { isPackaged: false }, })); -vi.mock("child_process", () => ({ execFile })); +vi.mock("child_process", () => ({ execFile, spawn })); vi.mock("fs", () => ({ existsSync })); const { scanWorkspace, searchHybrid } = await import("./scanner"); +function mockSpawn(stdoutChunks: string[], exitCode = 0): void { + spawn.mockImplementation(() => { + const child = new EventEmitter() as EventEmitter & { + stdout: EventEmitter; + kill: ReturnType; + }; + child.stdout = new EventEmitter(); + child.kill = vi.fn(() => true); + + queueMicrotask(() => { + for (const chunk of stdoutChunks) child.stdout.emit("data", chunk); + child.emit("close", exitCode); + }); + + return child; + }); +} + describe("Rust scanner fallback", () => { beforeEach(() => vi.clearAllMocks()); it("returns null when the engine is absent", async () => { existsSync.mockReturnValue(false); expect(await scanWorkspace("C:\\workspace")).toBeNull(); + expect(spawn).not.toHaveBeenCalled(); }); it("returns null when scan output is invalid", async () => { existsSync.mockReturnValue(true); - execFile.mockImplementation((_file, _args, _options, callback) => - callback(null, "not-json\n", ""), - ); + mockSpawn(["not-json\n"]); expect(await scanWorkspace("C:\\workspace")).toBeNull(); }); it("parses JSONL chunks", async () => { existsSync.mockReturnValue(true); - execFile.mockImplementation((_file, _args, _options, callback) => - callback(null, '{"id":"1","filePath":"a.ts","startLine":1,"endLine":1,"code":"x"}\n', ""), - ); + mockSpawn(['{"id":"1","filePath":"a.ts","startLine":1,"endLine":1,"code":"x"}\n']); expect(await scanWorkspace("C:\\workspace")).toHaveLength(1); }); + it("returns null when the scan process exits unsuccessfully", async () => { + existsSync.mockReturnValue(true); + mockSpawn([], 1); + expect(await scanWorkspace("C:\\workspace")).toBeNull(); + }); + + it("parses JSONL chunks split across stdout events", async () => { + existsSync.mockReturnValue(true); + mockSpawn([ + '{"id":"1","filePath":"a.ts",', + '"startLine":1,"endLine":1,"code":"x"}\n', + '{"id":"2","filePath":"b.ts","startLine":2,"endLine":2,"code":"y"}', + ]); + expect(await scanWorkspace("C:\\workspace")).toHaveLength(2); + }); + it("falls back when hybrid search process fails", async () => { existsSync.mockReturnValue(true); execFile.mockImplementation((_file, _args, _options, callback) => diff --git a/src/main/indexer/scanner.ts b/src/main/indexer/scanner.ts index 334a9b9..5863f0f 100644 --- a/src/main/indexer/scanner.ts +++ b/src/main/indexer/scanner.ts @@ -1,5 +1,5 @@ import { app } from "electron"; -import { execFile } from "child_process"; +import { execFile, spawn } from "child_process"; import { existsSync } from "fs"; import { join } from "path"; import type { IndexChunk, SearchResult } from "./types"; @@ -28,23 +28,66 @@ export async function scanWorkspace( return new Promise((resolve) => { const args = ["scan", workspacePath]; if (cachePath) args.push(cachePath); - execFile(binaryPath, args, { maxBuffer: 100 * 1024 * 1024 }, (error, stdout) => { - if (error) { - console.warn("[indexer] Rust scanner failed, using TypeScript fallback:", error); - resolve(null); - return; - } + const chunks: IndexChunk[] = []; + let buffer = ""; + let settled = false; + + const finish = (result: IndexChunk[] | null): void => { + if (settled) return; + settled = true; + resolve(result); + }; + + const parseLine = (line: string): boolean => { + const trimmed = line.trim(); + if (!trimmed) return true; try { - const chunks = stdout - .split(/\r?\n/) - .filter(Boolean) - .map((line) => JSON.parse(line) as IndexChunk); - resolve(chunks); + chunks.push(JSON.parse(trimmed) as IndexChunk); + return true; } catch (error) { console.warn("[indexer] Invalid Rust scanner output, using TypeScript fallback:", error); - resolve(null); + return false; } + }; + + const child = spawn(binaryPath, args, { windowsHide: true }); + + child.stdout.on("data", (data: Buffer | string) => { + if (settled) return; + buffer += data.toString(); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const line of lines) { + if (!parseLine(line)) { + child.kill(); + finish(null); + return; + } + } + }); + + child.on("error", (error) => { + console.warn("[indexer] Rust scanner failed, using TypeScript fallback:", error); + finish(null); + }); + + child.on("close", (code) => { + if (settled) return; + if (code !== 0) { + const error = new Error(`Rust scanner exited with code ${code ?? "unknown"}`); + console.warn("[indexer] Rust scanner failed, using TypeScript fallback:", error); + finish(null); + return; + } + + if (buffer && !parseLine(buffer)) { + finish(null); + return; + } + + finish(chunks); }); }); } From b714e63f8218a1f5a26ce50c1ae58557b8b7a498 Mon Sep 17 00:00:00 2001 From: Matecore Date: Mon, 29 Jun 2026 15:59:51 -0300 Subject: [PATCH 2/3] Add conservative IPC guardrails Signed-off-by: Matecore --- src/main/index.ts | 7 +++- src/main/ipc/fs.ts | 44 ++++++++++++---------- src/main/ipc/system.ts | 62 +++++++++++++++++++++++-------- src/main/ipc/validation.test.ts | 46 +++++++++++++++++++++++ src/main/ipc/validation.ts | 45 ++++++++++++++++++++++ src/renderer/src/utils/ai/chat.ts | 5 --- 6 files changed, 169 insertions(+), 40 deletions(-) create mode 100644 src/main/ipc/validation.test.ts create mode 100644 src/main/ipc/validation.ts diff --git a/src/main/index.ts b/src/main/index.ts index 85efbbc..37f51f8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,6 +5,7 @@ import { appendFileSync, mkdirSync, existsSync } from "fs"; import { electronApp, optimizer, is } from "@electron-toolkit/utils"; import { registerIpcHandlers } from "./ipc"; import { cleanupTerminals } from "./ipc/terminal"; +import { ipcWarn, normalizeIpcUrl } from "./ipc/validation"; function getLogPath(): string { const logsDir = is.dev ? join(__dirname, "../../logs") : join(app.getPath("userData"), "logs"); @@ -68,7 +69,11 @@ function createWindow(): void { // }); mainWindow.webContents.setWindowOpenHandler((details) => { - shell.openExternal(details.url); + try { + shell.openExternal(normalizeIpcUrl(details.url, ["http:", "https:", "mailto:"])); + } catch (error) { + ipcWarn("window:openExternal", error); + } return { action: "deny" }; }); diff --git a/src/main/ipc/fs.ts b/src/main/ipc/fs.ts index dcf810a..a1404d8 100644 --- a/src/main/ipc/fs.ts +++ b/src/main/ipc/fs.ts @@ -29,6 +29,7 @@ import { readTopographicContent, } from "./fs/topographic"; import { ensureTopographicCache } from "./fs/graphCache"; +import { ipcWarn, normalizeIpcPath } from "./validation"; function decomposeFileToSections(filePath: string, content: string): StructuralNode[] { const lines = content.split("\n"); @@ -258,28 +259,27 @@ export function registerFsHandlers(): void { ipcMain.handle("fs:readFileBase64", (_event, filePath: string) => { let finalPath = filePath; try { + if (typeof filePath !== "string" || !filePath.trim() || filePath.includes("\0")) return null; if (filePath.startsWith("resources/") || filePath.startsWith("resources\\")) { finalPath = app.isPackaged ? join(process.resourcesPath, filePath) : join(process.cwd(), filePath); + } else { + finalPath = normalizeIpcPath(filePath, "filePath"); } return readFileSync(finalPath).toString("base64"); } catch (e: any) { - try { - require("fs").appendFileSync( - "C:\\Users\\iange\\codeclubDebug.txt", - finalPath + " -> " + e.message + "\n", - ); - } catch {} + ipcWarn("fs:readFileBase64", e); return null; } }); ipcMain.handle("fs:copyFile", (_event, src: string, dest: string) => { try { - cpSync(src, dest, { recursive: true }); + cpSync(normalizeIpcPath(src, "src"), normalizeIpcPath(dest, "dest"), { recursive: true }); return true; - } catch { + } catch (error) { + ipcWarn("fs:copyFile", error); return false; } }); @@ -447,44 +447,49 @@ export function registerFsHandlers(): void { ipcMain.handle("fs:createFile", (_event, filePath: string) => { try { - writeFileSync(filePath, "", "utf-8"); + writeFileSync(normalizeIpcPath(filePath, "filePath"), "", "utf-8"); return true; - } catch { + } catch (error) { + ipcWarn("fs:createFile", error); return false; } }); ipcMain.handle("fs:createDir", (_event, dirPath: string) => { try { - mkdirSync(dirPath, { recursive: true }); + mkdirSync(normalizeIpcPath(dirPath, "dirPath"), { recursive: true }); return true; - } catch { + } catch (error) { + ipcWarn("fs:createDir", error); return false; } }); ipcMain.handle("fs:rename", (_event, oldPath: string, newPath: string) => { try { - renameSync(oldPath, newPath); + renameSync(normalizeIpcPath(oldPath, "oldPath"), normalizeIpcPath(newPath, "newPath")); return true; - } catch { + } catch (error) { + ipcWarn("fs:rename", error); return false; } }); ipcMain.handle("fs:delete", async (_event, targetPath: string) => { try { + const safeTarget = normalizeIpcPath(targetPath, "targetPath"); const binaryPath = getScanBinaryPath(); if (!existsSync(binaryPath)) { - rmSync(targetPath, { recursive: true, force: true }); + rmSync(safeTarget, { recursive: true, force: true }); return true; } return new Promise((resolve) => { - execFile(binaryPath, ["io", "delete-file", targetPath], (error) => { + execFile(binaryPath, ["io", "delete-file", safeTarget], (error) => { resolve(!error); }); }); - } catch { + } catch (error) { + ipcWarn("fs:delete", error); return false; } }); @@ -528,8 +533,9 @@ export function registerFsHandlers(): void { ipcMain.handle("fs:exists", (_event, targetPath: string) => { try { - return existsSync(targetPath); - } catch { + return existsSync(normalizeIpcPath(targetPath, "targetPath")); + } catch (error) { + ipcWarn("fs:exists", error); return false; } }); diff --git a/src/main/ipc/system.ts b/src/main/ipc/system.ts index 4fdb89d..213fb51 100644 --- a/src/main/ipc/system.ts +++ b/src/main/ipc/system.ts @@ -20,6 +20,13 @@ import { syncComponentToInstances } from "../../shared/designComponents"; import { buildDesignExportFiles } from "../../shared/designExport"; import { resolveDesignTokens } from "../../shared/designTokens"; import { EMPTY_TOKENS, type DesignTokenCollection } from "../../shared/design"; +import { + ipcWarn, + isLikelyBase64, + normalizeIpcPath, + normalizeIpcString, + normalizeIpcUrl, +} from "./validation"; const designPageCache = new Map(); const DESIGN_CACHE_LIMIT = 32; @@ -398,7 +405,12 @@ export function registerSystemHandlers(): void { }); ipcMain.handle("system:openLink", (_event, url: string) => { - return shell.openExternal(url); + try { + return shell.openExternal(normalizeIpcUrl(url, ["http:", "https:", "mailto:"])); + } catch (error) { + ipcWarn("system:openLink", error); + return undefined; + } }); ipcMain.handle("system:openEmail", (_event, email: string) => { @@ -417,7 +429,7 @@ export function registerSystemHandlers(): void { ipcMain.handle("system:fetch", async (_event, url: string, options?: any) => { try { - const res = await fetch(url, options); + const res = await fetch(normalizeIpcUrl(url), options); const data = await res.text(); return { ok: res.ok, @@ -427,6 +439,7 @@ export function registerSystemHandlers(): void { headers: Object.fromEntries((res.headers as any).entries()), }; } catch (err: any) { + ipcWarn("system:fetch", err); return { ok: false, error: err.message }; } }); @@ -434,8 +447,16 @@ export function registerSystemHandlers(): void { ipcMain.handle("system:fetchStream", (event, url: string, options?: any) => { const streamId = Math.random().toString(36).slice(2); const win = BrowserWindow.fromWebContents(event.sender); + let safeUrl: string; + try { + safeUrl = normalizeIpcUrl(url); + } catch (error: any) { + ipcWarn("system:fetchStream", error); + queueMicrotask(() => win?.webContents.send(`stream:error:${streamId}`, error.message)); + return streamId; + } - fetch(url, options) + fetch(safeUrl, options) .then(async (res) => { if (!res.ok) { const err = await res.text(); @@ -953,25 +974,30 @@ export function registerSystemHandlers(): void { ); ipcMain.handle("system:designExportFiles", (_event, workspacePath: string, pageId: string) => { - const page = readDesignPage(workspacePath, pageId); - if (!page) return { ok: false, error: "Page not found." }; try { + const safeWorkspace = normalizeIpcPath(workspacePath, "workspacePath"); + const safePageId = normalizeIpcString(pageId, "pageId"); + const page = readDesignPage(safeWorkspace, safePageId); + if (!page) return { ok: false, error: "Page not found." }; const files = buildDesignExportFiles(page); - const exportDir = join(designPaths(workspacePath).root, "exports", files.pageName); + const exportDir = join(designPaths(safeWorkspace).root, "exports", files.pageName); mkdirSync(exportDir, { recursive: true }); writeFileSync(join(exportDir, `${files.pageName}.tsx`), files.tsx); writeFileSync(join(exportDir, `${files.pageName}.module.css`), files.css); writeFileSync(join(exportDir, `${files.pageName}-tokens.json`), files.tokensJson); return { ok: true, path: exportDir, name: files.pageName }; } catch (error: any) { + ipcWarn("system:designExportFiles", error); return { ok: false, error: error?.message || "Export error." }; } }); ipcMain.handle("system:designExportPng", (_event, workspacePath: string, pageId: string) => { - const page = readDesignPage(workspacePath, pageId); - if (!page) return { ok: false, error: "Page not found." }; try { + const safeWorkspace = normalizeIpcPath(workspacePath, "workspacePath"); + const safePageId = normalizeIpcString(pageId, "pageId"); + const page = readDesignPage(safeWorkspace, safePageId); + if (!page) return { ok: false, error: "Page not found." }; const visible = page.layers.filter((l) => l.visible && l.type !== "group"); const w = Math.max(1, ...visible.map((l) => l.x + l.width)); const h = Math.max(1, ...visible.map((l) => l.y + l.height)); @@ -980,7 +1006,7 @@ export function registerSystemHandlers(): void { .replace(/[^a-zA-Z0-9]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, "") || "design"; - const exportDir = join(designPaths(workspacePath).root, "exports", safeName); + const exportDir = join(designPaths(safeWorkspace).root, "exports", safeName); const exportPath = join(exportDir, `${safeName}.png`); return { ok: true, @@ -990,22 +1016,28 @@ export function registerSystemHandlers(): void { exportPath, }; } catch (error: any) { + ipcWarn("system:designExportPng", error); return { ok: false, error: error?.message || "Bounds error." }; } }); ipcMain.handle("system:designWritePng", (_event, exportPath: string, base64data: string) => { try { - const dir = exportPath.slice( + const safeExportPath = normalizeIpcPath(exportPath, "exportPath"); + if (!isLikelyBase64(base64data, 100 * 1024 * 1024)) { + return { ok: false, error: "Invalid PNG data." }; + } + const dir = safeExportPath.slice( 0, - exportPath.lastIndexOf("\\") !== -1 - ? exportPath.lastIndexOf("\\") - : exportPath.lastIndexOf("/"), + safeExportPath.lastIndexOf("\\") !== -1 + ? safeExportPath.lastIndexOf("\\") + : safeExportPath.lastIndexOf("/"), ); if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true }); - writeFileSync(exportPath, Buffer.from(base64data, "base64")); - return { ok: true, path: exportPath }; + writeFileSync(safeExportPath, Buffer.from(base64data, "base64")); + return { ok: true, path: safeExportPath }; } catch (error: any) { + ipcWarn("system:designWritePng", error); return { ok: false, error: error?.message || "Write error." }; } }); diff --git a/src/main/ipc/validation.test.ts b/src/main/ipc/validation.test.ts new file mode 100644 index 0000000..fc090b9 --- /dev/null +++ b/src/main/ipc/validation.test.ts @@ -0,0 +1,46 @@ +import { isAbsolute } from "path"; +import { describe, expect, it } from "vite-plus/test"; +import { + isLikelyBase64, + normalizeIpcPath, + normalizeIpcString, + normalizeIpcUrl, +} from "./validation"; + +describe("IPC validation", () => { + it("normalizes non-empty strings", () => { + expect(normalizeIpcString(" ok ", "value")).toBe("ok"); + }); + + it("rejects empty strings and null bytes", () => { + expect(() => normalizeIpcString(" ", "value")).toThrow("required"); + expect(() => normalizeIpcString("a\0b", "value")).toThrow("null byte"); + }); + + it("resolves paths without requiring workspace scope", () => { + const normalized = normalizeIpcPath("src/main/index.ts"); + expect(isAbsolute(normalized)).toBe(true); + expect(normalized.replace(/\\/g, "/")).toContain("src/main/index.ts"); + }); + + it("allows http and https URLs by default", () => { + expect(normalizeIpcUrl("https://example.com/path")).toBe("https://example.com/path"); + expect(normalizeIpcUrl("http://localhost:11434/api")).toBe("http://localhost:11434/api"); + }); + + it("rejects unsafe URL protocols", () => { + expect(() => normalizeIpcUrl("file:///C:/secret.txt")).toThrow("protocol"); + expect(() => normalizeIpcUrl("javascript:alert(1)")).toThrow("protocol"); + }); + + it("allows mailto only when explicitly requested", () => { + expect(() => normalizeIpcUrl("mailto:test@example.com")).toThrow("protocol"); + expect(normalizeIpcUrl("mailto:test@example.com", ["mailto:"])).toBe("mailto:test@example.com"); + }); + + it("checks base64 shape and size", () => { + expect(isLikelyBase64("aGVsbG8=", 16)).toBe(true); + expect(isLikelyBase64("not base64!", 16)).toBe(false); + expect(isLikelyBase64("aGVsbG8=", 1)).toBe(false); + }); +}); diff --git a/src/main/ipc/validation.ts b/src/main/ipc/validation.ts new file mode 100644 index 0000000..f67cb25 --- /dev/null +++ b/src/main/ipc/validation.ts @@ -0,0 +1,45 @@ +import { resolve } from "path"; + +const MAX_STRING_LENGTH = 16 * 1024 * 1024; + +export function ipcWarn(scope: string, error: unknown): void { + const message = error instanceof Error ? error.message : String(error); + console.warn(`[ipc:${scope}] ${message}`); +} + +export function normalizeIpcString(value: unknown, label: string): string { + if (typeof value !== "string") throw new Error(`${label} must be a string`); + const trimmed = value.trim(); + if (!trimmed) throw new Error(`${label} is required`); + if (trimmed.includes("\0")) throw new Error(`${label} contains an invalid null byte`); + if (trimmed.length > MAX_STRING_LENGTH) throw new Error(`${label} is too large`); + return trimmed; +} + +export function normalizeIpcPath(value: unknown, label = "path"): string { + const path = normalizeIpcString(value, label); + return resolve(path); +} + +export function normalizeIpcUrl( + value: unknown, + protocols: readonly string[] = ["http:", "https:"], +): string { + const raw = normalizeIpcString(value, "url"); + let parsed: URL; + try { + parsed = new URL(raw); + } catch { + throw new Error("url is invalid"); + } + if (!protocols.includes(parsed.protocol)) { + throw new Error(`url protocol is not allowed: ${parsed.protocol}`); + } + return parsed.toString(); +} + +export function isLikelyBase64(value: unknown, maxBytes: number): value is string { + if (typeof value !== "string" || !value) return false; + if (value.length > Math.ceil(maxBytes * 1.4)) return false; + return /^[A-Za-z0-9+/=\r\n]+$/.test(value); +} diff --git a/src/renderer/src/utils/ai/chat.ts b/src/renderer/src/utils/ai/chat.ts index 4f6fa05..1dfbbaa 100644 --- a/src/renderer/src/utils/ai/chat.ts +++ b/src/renderer/src/utils/ai/chat.ts @@ -93,7 +93,6 @@ export async function* streamChatCompletion( await ensureCacheLoaded(); const cacheKey = JSON.stringify({ messages, model: config.model }); if (chatCache.has(cacheKey)) { - console.log("[Semantic Cache] Yielding cached stream completion"); const cachedEvents = chatCache.get(cacheKey)!; for (const event of cachedEvents) { yield event; @@ -107,7 +106,6 @@ export async function* streamChatCompletion( if (tools && tools.length > 0) body.tools = tools; if (config.reasoning_effort) body.reasoning_effort = config.reasoning_effort; - console.log(`[Stream] Starting: ${config.model} @ ${url}`, body); const streamId = await window.api.proxyFetchStream(url, { method: "POST", headers: { @@ -118,7 +116,6 @@ export async function* streamChatCompletion( }, body: JSON.stringify(body), }); - console.log(`[Stream] ID: ${streamId}`); const toolCallAccum: Record = {}; let usage: UsageInfo | undefined; @@ -144,7 +141,6 @@ export async function* streamChatCompletion( if (!trimmed || !trimmed.startsWith("data: ")) continue; const data = trimmed.slice(6); if (data === "[DONE]") { - console.log(`[Stream] ${streamId} DONE via [DONE] marker`); continue; } @@ -217,7 +213,6 @@ export async function* streamChatCompletion( }); const unDone = window.api.onStreamDone(streamId, () => { - console.log(`[Stream] ${streamId} DONE via socket close`); isDone = true; resolveNext?.(); }); From 307f6bcadcb6bce622369cd4e988f163fe95b3b5 Mon Sep 17 00:00:00 2001 From: Matecore Date: Tue, 30 Jun 2026 13:35:37 -0300 Subject: [PATCH 3/3] fix: override expr-eval -> expr-eval-fork to fix Dependabot alerts - Replace expr-eval@2.0.2 (unmaintained, 2 high CVEs) with expr-eval-fork@3.0.3 via npm overrides - Fixes GHSA-jc85-fpwf-qm7x (Code Injection, CVSS 8.6) and GHSA-8gw3-rxh4-v6jx (Prototype Pollution, CVSS 7.3) Signed-off-by: Matecore --- package-lock.json | 16 ++++++++++------ package.json | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62cae9b..a3988db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1685,6 +1685,16 @@ "@webgpu/types": "0.1.21" } }, + "node_modules/@open-pencil/core/node_modules/expr-eval": { + "name": "expr-eval-fork", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/expr-eval-fork/-/expr-eval-fork-3.0.3.tgz", + "integrity": "sha512-BhC+hbc5lIVjygr840n5DEkW3MQq7H9o+mc1/N7Z5uIiCFVyESLL5DIE7LNq4CYUNxy+XjA+3jRrL/h0Kt2xcg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/@open-pencil/core/node_modules/yoga-layout": { "name": "@open-pencil/yoga-layout", "version": "3.3.0-grid.3", @@ -6390,12 +6400,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/expr-eval": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expr-eval/-/expr-eval-2.0.2.tgz", - "integrity": "sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==", - "license": "MIT" - }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", diff --git a/package.json b/package.json index c5f3a8e..e219763 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,8 @@ "vite-plus": "latest" }, "overrides": { - "dompurify": "$dompurify" + "dompurify": "$dompurify", + "expr-eval": "npm:expr-eval-fork@^3.0.1" }, "devEngines": { "packageManager": {