diff --git a/bun.lock b/bun.lock index 525904f5c..8c6597693 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,9 @@ "lru-cache": "^11.2.2", "markdown-it": "^14.1.0", "minimist": "^1.2.8", + "rehype-harden": "^1.1.5", "source-map-support": "^0.5.21", + "streamdown": "^1.4.0", "undici": "^7.16.0", "write-file-atomic": "^6.0.0", "ws": "^8.18.3", @@ -91,10 +93,8 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", - "react-markdown": "^10.1.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", - "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.13.0", @@ -1718,8 +1718,6 @@ "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], @@ -2090,6 +2088,8 @@ "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + "lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], @@ -2492,12 +2492,12 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "rehype-harden": ["rehype-harden@1.1.5", "", {}, "sha512-JrtBj5BVd/5vf3H3/blyJatXJbzQfRT9pJBmjafbTaPouQCAKxHwRyCc7dle9BXQKxv4z1OzZylz/tNamoiG3A=="], + "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], - "release-zalgo": ["release-zalgo@1.0.0", "", { "dependencies": { "es6-error": "^4.0.1" } }, "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], @@ -2646,6 +2646,8 @@ "storybook": ["storybook@8.6.14", "", { "dependencies": { "@storybook/core": "8.6.14" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": { "sb": "./bin/index.cjs", "storybook": "./bin/index.cjs", "getstorybook": "./bin/index.cjs" } }, "sha512-sVKbCj/OTx67jhmauhxc2dcr1P+yOgz/x3h0krwjyMgdc5Oubvxyg4NYDZmzAw+ym36g/lzH8N0Ccp4dwtdfxw=="], + "streamdown": ["streamdown@1.4.0", "", { "dependencies": { "clsx": "^2.1.1", "katex": "^0.16.22", "lucide-react": "^0.542.0", "marked": "^16.2.1", "mermaid": "^11.11.0", "react-markdown": "^10.1.0", "rehype-harden": "^1.1.5", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.12.2", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-ylhDSQ4HpK5/nAH9v7OgIIdGJxlJB2HoYrYkJNGrO8lMpnWuKUcrz/A8xAMwA6eILA27469vIavcOTjmxctrKg=="], + "string-length": ["string-length@5.0.1", "", { "dependencies": { "char-regex": "^2.0.0", "strip-ansi": "^7.0.1" } }, "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], diff --git a/package.json b/package.json index c327bec83..64a4c210a 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,9 @@ "lru-cache": "^11.2.2", "markdown-it": "^14.1.0", "minimist": "^1.2.8", + "rehype-harden": "^1.1.5", "source-map-support": "^0.5.21", + "streamdown": "^1.4.0", "undici": "^7.16.0", "write-file-atomic": "^6.0.0", "ws": "^8.18.3", @@ -131,10 +133,8 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", - "react-markdown": "^10.1.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", - "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.13.0", diff --git a/src/components/Messages/MarkdownCore.tsx b/src/components/Messages/MarkdownCore.tsx index 93f6d176c..652d5f05b 100644 --- a/src/components/Messages/MarkdownCore.tsx +++ b/src/components/Messages/MarkdownCore.tsx @@ -1,11 +1,11 @@ import React, { useMemo } from "react"; -import ReactMarkdown from "react-markdown"; -import type { PluggableList } from "unified"; +import { Streamdown } from "streamdown"; +import type { Pluggable } from "unified"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; import rehypeRaw from "rehype-raw"; -import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; +import { harden } from "rehype-harden"; import "katex/dist/katex.min.css"; import { normalizeMarkdown } from "./MarkdownStyles"; import { markdownComponents } from "./MarkdownComponents"; @@ -16,24 +16,24 @@ interface MarkdownCoreProps { } // Plugin arrays are defined at module scope to maintain stable references. -// ReactMarkdown treats new array references as changes requiring full re-parse. -const REMARK_PLUGINS = [remarkGfm, remarkMath]; - -// Sanitization schema: whitelist only safe HTML elements -// This prevents XSS attacks while allowing
/ toggles -const SANITIZE_SCHEMA = { - ...defaultSchema, - tagNames: [...(defaultSchema.tagNames ?? []), "details", "summary"], - attributes: { - ...defaultSchema.attributes, - details: ["open"], // Allow 'open' attribute for default-expanded state - }, -}; +// Streamdown treats new array references as changes requiring full re-parse. +const REMARK_PLUGINS: Pluggable[] = [ + [remarkGfm, {}], + [remarkMath, { singleDollarTextMath: false }], +]; -const REHYPE_PLUGINS: PluggableList = [ - rehypeRaw, // Parse HTML elements - [rehypeSanitize, SANITIZE_SCHEMA], // Sanitize to whitelist only - rehypeKatex, // Render math (must be after sanitization) +const REHYPE_PLUGINS: Pluggable[] = [ + rehypeRaw, // Parse HTML elements first + [ + harden, // Sanitize after parsing raw HTML to prevent XSS + { + allowedImagePrefixes: ["*"], + allowedLinkPrefixes: ["*"], + defaultOrigin: undefined, + allowDataImages: true, + }, + ], + [rehypeKatex, { errorColor: "var(--color-muted-foreground)" }], // Render math ]; /** @@ -48,13 +48,15 @@ export const MarkdownCore = React.memo(({ content, children } return ( <> - {normalizedContent} - + {children} ); diff --git a/src/components/Messages/MessageWindow.tsx b/src/components/Messages/MessageWindow.tsx index 85b752de3..c89f9192d 100644 --- a/src/components/Messages/MessageWindow.tsx +++ b/src/components/Messages/MessageWindow.tsx @@ -122,7 +122,7 @@ export const MessageWindow: React.FC = ({ )} -
+
{showJson ? (
             {JSON.stringify(message, null, 2)}
diff --git a/src/styles/globals.css b/src/styles/globals.css
index b037e429f..2f08dafe2 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -1,4 +1,5 @@
 @import "tailwindcss";
+@source "../node_modules/streamdown/dist/index.js";
 
 @theme {
   /* Mode Colors */
@@ -466,9 +467,7 @@ code {
   font-size: 13px;
 }
 
-.markdown-content p {
-  margin: 0.8em 0;
-}
+
 
 /* Remove default margins on first and last elements */
 .markdown-content > :first-child {