From c972ee779e719bd2690ba576cfd41c930b2e1842 Mon Sep 17 00:00:00 2001
From: Qoder-AI <215799558+Qoder-AI@users.noreply.github.com>
Date: Sat, 24 Oct 2026 03:35:17 +0800
Subject: [PATCH 1/3] fix(web): keep URL schemes out of file link parsing
---
web/src/lib/chat-resources.ts | 3 ++-
web/src/v2/components/resource-link.test.tsx | 7 +++++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/web/src/lib/chat-resources.ts b/web/src/lib/chat-resources.ts
index 3de792b0..a6415486 100644
--- a/web/src/lib/chat-resources.ts
+++ b/web/src/lib/chat-resources.ts
@@ -76,11 +76,12 @@ export function parseProjectFileLink(href: string): FileLinkTarget | null {
if (/^file:/iu.test(raw)) {
if (!/^file:\/\/\//iu.test(raw)) return null;
raw = raw.slice(7).replace(/^\/([a-z]:)/iu, "$1");
- } else if (/^[a-z][a-z\d+.-]*:/iu.test(raw) && !/^[a-z]:[\\/]/iu.test(raw) && !/^.+\.\w+:\d+(?::\d+)?$/u.test(raw)) return null;
+ }
let line: number | undefined;
const location = /(?:#L|:)(\d+)(?::\d+)?(?:-L?\d+)?$/iu.exec(raw);
if (location) { line = Number(location[1]); raw = raw.slice(0, location.index); }
if (line !== undefined && (!Number.isSafeInteger(line) || line < 1)) return null;
+ if (/^[a-z][a-z\d+.-]*:/iu.test(raw) && !/^[a-z]:[\\/]/iu.test(raw)) return null;
raw = raw.replace(/\\/gu, "/").replace(/^(\.\/)+/u, "");
if (!raw || raw === "." || raw.split("/").includes("..") || /[?#]/u.test(raw)) return null;
return { path: raw, line };
diff --git a/web/src/v2/components/resource-link.test.tsx b/web/src/v2/components/resource-link.test.tsx
index e4d4e012..20636a0e 100644
--- a/web/src/v2/components/resource-link.test.tsx
+++ b/web/src/v2/components/resource-link.test.tsx
@@ -163,6 +163,13 @@ it("rejects executable URL schemes and preserves absolute local paths for native
expect(parseProjectFileLink("vscode://open-file")).toBeNull();
});
+it("keeps web links with line suffixes out of workspace file parsing", () => {
+ expect(parseProjectFileLink("https://example.com/report.md:12")).toBeNull();
+ expect(parseProjectFileLink("mailto:user@example.com:12")).toBeNull();
+ expect(parseProjectFileLink("src/report.md:12")).toEqual({ path: "src/report.md", line: 12 });
+ expect(parseProjectFileLink("C:/project/report.md:12")).toEqual({ path: "C:/project/report.md", line: 12 });
+});
+
function render(ui: ReactElement, options?: RenderOptions) {
localStorage.setItem("prayu.locale.v1", "zh-CN");
const Wrapper=options?.wrapper;
From dfbf274f99121a54892864a66e5934392f3263cd Mon Sep 17 00:00:00 2001
From: Qiyuanqiii <267806965+Qiyuanqiii@users.noreply.github.com>
Date: Fri, 23 Oct 2026 03:19:45 +0800
Subject: [PATCH 2/3] fix(web): use patched DOMPurify in Monaco builds
---
web/package-lock.json | 7 ++++---
web/package.json | 3 ++-
web/src/lib/monaco-sanitizer.test.ts | 21 +++++++++++++++++++++
web/vite.config.ts | 24 ++++++++++++++++++++++--
4 files changed, 49 insertions(+), 6 deletions(-)
create mode 100644 web/src/lib/monaco-sanitizer.test.ts
diff --git a/web/package-lock.json b/web/package-lock.json
index 9d8c1bd7..f662194f 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -20,6 +20,7 @@
"@tiptap/starter-kit": "3.32.0",
"@xterm/addon-fit": "0.11.0",
"@xterm/xterm": "6.0.0",
+ "dompurify": "3.4.17",
"highlight.js": "11.11.1",
"lucide-react": "1.31.0",
"monaco-editor": "0.56.0",
@@ -1840,9 +1841,9 @@
"peer": true
},
"node_modules/dompurify": {
- "version": "3.4.16",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz",
- "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==",
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz",
+ "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
diff --git a/web/package.json b/web/package.json
index 89d9de27..ea6bb4dc 100644
--- a/web/package.json
+++ b/web/package.json
@@ -27,6 +27,7 @@
"@tiptap/starter-kit": "3.32.0",
"@xterm/addon-fit": "0.11.0",
"@xterm/xterm": "6.0.0",
+ "dompurify": "3.4.17",
"highlight.js": "11.11.1",
"lucide-react": "1.31.0",
"monaco-editor": "0.56.0",
@@ -56,7 +57,7 @@
"js-yaml": "4.3.2"
},
"brace-expansion": "5.0.12",
- "dompurify": "3.4.16",
+ "dompurify": "3.4.17",
"nanoid": "3.3.18",
"postcss": "8.5.25",
"undici": "7.29.1"
diff --git a/web/src/lib/monaco-sanitizer.test.ts b/web/src/lib/monaco-sanitizer.test.ts
new file mode 100644
index 00000000..a6fb29d0
--- /dev/null
+++ b/web/src/lib/monaco-sanitizer.test.ts
@@ -0,0 +1,21 @@
+import DOMPurify from "dompurify";
+// @ts-expect-error Monaco ships this runtime sanitizer without declarations.
+import { safeSetInnerHtml, sanitizeHtml } from "monaco-editor/base/browser/domSanitize.js";
+
+describe("Monaco sanitizer", () => {
+ it.each(["trusted HTML", "DOM fragment"])("uses the installed DOMPurify for %s", (output) => {
+ const sanitize = vi.spyOn(DOMPurify, "sanitize");
+ const dirty = '
Kept formatting
' +
+ '
unsafe' +
+ 'safe';
+ const target = document.createElement("div");
+
+ if (output === "trusted HTML") target.innerHTML = String(sanitizeHtml(dirty));
+ else safeSetInnerHtml(target, dirty);
+
+ expect(sanitize).toHaveBeenCalled();
+ expect(target.querySelector("strong")).toHaveTextContent("formatting");
+ expect(target.querySelector('script, [onerror], a[href^="javascript:"]')).toBeNull();
+ expect(target.querySelector('a[href="https://example.com"]')).toHaveTextContent("safe");
+ });
+});
diff --git a/web/vite.config.ts b/web/vite.config.ts
index 7faf222b..fd8574df 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -1,7 +1,23 @@
+import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
-import { loadEnv } from "vite";
+import { loadEnv, normalizePath, type Plugin } from "vite";
import { defineConfig } from "vitest/config";
+// npm overrides cannot replace Monaco's embedded copy. Redirect its sanitizer import
+// in both Vite and the separate dependency prebundler to the installed DOMPurify.
+const monacoSanitizer = normalizePath(fileURLToPath(import.meta.resolve("monaco-editor/base/browser/domSanitize.js")));
+const monacoDOMPurify: Plugin = {
+ name: "monaco-dompurify",
+ enforce: "pre",
+ async resolveId(source, importer) {
+ if (source === "./dompurify/dompurify.js" && importer && normalizePath(importer) === monacoSanitizer) {
+ const resolved = await this.resolve("dompurify", importer, { skipSelf: true });
+ if (!resolved) throw new Error("Cannot resolve installed DOMPurify for the Monaco sanitizer");
+ return resolved;
+ }
+ },
+};
+
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, ".", "");
const apiTarget = env.CYBERAGENT_API_TARGET || "http://127.0.0.1:8765";
@@ -19,7 +35,10 @@ export default defineConfig(({ mode }) => {
};
return {
- plugins: [react()],
+ plugins: [react(), monacoDOMPurify],
+ optimizeDeps: {
+ rolldownOptions: { plugins: [monacoDOMPurify] },
+ },
server: {
host: "127.0.0.1",
port: 5173,
@@ -39,6 +58,7 @@ export default defineConfig(({ mode }) => {
},
test: {
include: ["src/**/*.{test,spec}.{ts,tsx}"],
+ server: { deps: { inline: ["monaco-editor"] } },
environment: "jsdom",
globals: true,
setupFiles: "./src/test/setup.ts",
From aa882e1536955f114e9684d50102ea10d62a7bff Mon Sep 17 00:00:00 2001
From: Qiyuanqiii <267806965+Qiyuanqiii@users.noreply.github.com>
Date: Tue, 27 Oct 2026 13:05:48 +0800
Subject: [PATCH 3/3] fix(web): patch Vitest and undici audit findings
---
web/package-lock.json | 96 +++++++++++++++++++++----------------------
web/package.json | 4 +-
2 files changed, 50 insertions(+), 50 deletions(-)
diff --git a/web/package-lock.json b/web/package-lock.json
index f662194f..03e7da3c 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -43,7 +43,7 @@
"openapi-typescript": "7.13.0",
"typescript": "5.9.3",
"vite": "8.3.4",
- "vitest": "4.1.11"
+ "vitest": "4.1.12"
}
},
"node_modules/@adobe/css-tools": {
@@ -1389,16 +1389,16 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz",
- "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz",
+ "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.11",
- "@vitest/utils": "4.1.11",
+ "@vitest/spy": "4.1.12",
+ "@vitest/utils": "4.1.12",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -1407,13 +1407,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz",
- "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz",
+ "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.11",
+ "@vitest/spy": "4.1.12",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -1434,9 +1434,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz",
- "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz",
+ "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1447,13 +1447,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz",
- "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz",
+ "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.11",
+ "@vitest/utils": "4.1.12",
"pathe": "^2.0.3"
},
"funding": {
@@ -1461,14 +1461,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz",
- "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz",
+ "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.11",
- "@vitest/utils": "4.1.11",
+ "@vitest/pretty-format": "4.1.12",
+ "@vitest/utils": "4.1.12",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -1477,9 +1477,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz",
- "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz",
+ "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1487,13 +1487,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz",
- "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz",
+ "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.11",
+ "@vitest/pretty-format": "4.1.12",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -4282,9 +4282,9 @@
}
},
"node_modules/undici": {
- "version": "7.29.1",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz",
- "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==",
+ "version": "7.30.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz",
+ "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4499,19 +4499,19 @@
}
},
"node_modules/vitest": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz",
- "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz",
+ "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.11",
- "@vitest/mocker": "4.1.11",
- "@vitest/pretty-format": "4.1.11",
- "@vitest/runner": "4.1.11",
- "@vitest/snapshot": "4.1.11",
- "@vitest/spy": "4.1.11",
- "@vitest/utils": "4.1.11",
+ "@vitest/expect": "4.1.12",
+ "@vitest/mocker": "4.1.12",
+ "@vitest/pretty-format": "4.1.12",
+ "@vitest/runner": "4.1.12",
+ "@vitest/snapshot": "4.1.12",
+ "@vitest/spy": "4.1.12",
+ "@vitest/utils": "4.1.12",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -4539,12 +4539,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.11",
- "@vitest/browser-preview": "4.1.11",
- "@vitest/browser-webdriverio": "4.1.11",
- "@vitest/coverage-istanbul": "4.1.11",
- "@vitest/coverage-v8": "4.1.11",
- "@vitest/ui": "4.1.11",
+ "@vitest/browser-playwright": "4.1.12",
+ "@vitest/browser-preview": "4.1.12",
+ "@vitest/browser-webdriverio": "^4.1.11",
+ "@vitest/coverage-istanbul": "4.1.12",
+ "@vitest/coverage-v8": "4.1.12",
+ "@vitest/ui": "4.1.12",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
diff --git a/web/package.json b/web/package.json
index ea6bb4dc..2f3eed72 100644
--- a/web/package.json
+++ b/web/package.json
@@ -50,7 +50,7 @@
"openapi-typescript": "7.13.0",
"typescript": "5.9.3",
"vite": "8.3.4",
- "vitest": "4.1.11"
+ "vitest": "4.1.12"
},
"overrides": {
"@redocly/openapi-core": {
@@ -60,6 +60,6 @@
"dompurify": "3.4.17",
"nanoid": "3.3.18",
"postcss": "8.5.25",
- "undici": "7.29.1"
+ "undici": "7.30.1"
}
}