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
103 changes: 52 additions & 51 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -49,16 +50,16 @@
"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": {
"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"
"undici": "7.30.1"
}
}
3 changes: 2 additions & 1 deletion web/src/lib/chat-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
21 changes: 21 additions & 0 deletions web/src/lib/monaco-sanitizer.test.ts
Original file line number Diff line number Diff line change
@@ -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 = '<p>Kept <strong>formatting</strong></p><script>alert(1)</script>' +
'<img src="data:," onerror="alert(2)"><a href="javascript:alert(3)">unsafe</a>' +
'<a href="https://example.com">safe</a>';
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");
});
});
7 changes: 7 additions & 0 deletions web/src/v2/components/resource-link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 22 additions & 2 deletions web/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand All @@ -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",
Expand Down
Loading