@katechat/ui 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.css +491 -0
- package/dist/cjs/index.css.map +7 -0
- package/dist/cjs/index.js +75305 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/esm/index.css +491 -0
- package/dist/esm/index.css.map +7 -0
- package/dist/esm/index.js +75304 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/index.css +1 -0
- package/dist/index.js +539 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +27 -4
- package/.prettierrc +0 -9
- package/esbuild.js +0 -56
- package/jest.config.js +0 -24
- package/postcss.config.cjs +0 -14
- package/src/__mocks__/fileMock.js +0 -1
- package/src/__mocks__/styleMock.js +0 -1
- package/src/components/chat/ChatMessagesContainer.module.scss +0 -77
- package/src/components/chat/ChatMessagesContainer.tsx +0 -151
- package/src/components/chat/ChatMessagesList.tsx +0 -216
- package/src/components/chat/index.ts +0 -4
- package/src/components/chat/input/ChatInput.module.scss +0 -113
- package/src/components/chat/input/ChatInput.tsx +0 -259
- package/src/components/chat/input/index.ts +0 -1
- package/src/components/chat/message/ChatMessage.Carousel.module.scss +0 -7
- package/src/components/chat/message/ChatMessage.module.scss +0 -378
- package/src/components/chat/message/ChatMessage.tsx +0 -271
- package/src/components/chat/message/ChatMessagePreview.tsx +0 -22
- package/src/components/chat/message/LinkedChatMessage.tsx +0 -64
- package/src/components/chat/message/MessageStatus.tsx +0 -38
- package/src/components/chat/message/controls/CopyMessageButton.tsx +0 -32
- package/src/components/chat/message/index.ts +0 -4
- package/src/components/icons/ProviderIcon.tsx +0 -49
- package/src/components/icons/index.ts +0 -1
- package/src/components/index.ts +0 -3
- package/src/components/modal/ImagePopup.tsx +0 -97
- package/src/components/modal/index.ts +0 -1
- package/src/controls/FileDropzone/FileDropzone.module.scss +0 -15
- package/src/controls/FileDropzone/FileDropzone.tsx +0 -120
- package/src/controls/index.ts +0 -1
- package/src/core/ai.ts +0 -1
- package/src/core/index.ts +0 -4
- package/src/core/message.ts +0 -59
- package/src/core/model.ts +0 -23
- package/src/core/user.ts +0 -8
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useIntersectionObserver.ts +0 -24
- package/src/hooks/useTheme.tsx +0 -66
- package/src/index.ts +0 -5
- package/src/lib/__tests__/markdown.parser.test.ts +0 -289
- package/src/lib/__tests__/markdown.parser.testUtils.ts +0 -31
- package/src/lib/__tests__/markdown.parser_sanitizeUrl.test.ts +0 -130
- package/src/lib/assert.ts +0 -14
- package/src/lib/markdown.parser.ts +0 -189
- package/src/setupTests.ts +0 -1
- package/src/types/scss.d.ts +0 -4
- package/tsconfig.json +0 -26
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
import { parseMarkdown, parseChatMessages, escapeHtml } from "../markdown.parser";
|
|
2
|
-
import { Message, MessageRole } from "../../core/message";
|
|
3
|
-
|
|
4
|
-
// Mock the sanitizeUrl function by accessing it through module internals
|
|
5
|
-
// Since sanitizeUrl is not exported, we'll test it indirectly through parseMarkdown
|
|
6
|
-
// or we can use jest.mock to access internals, but for now we'll test through integration
|
|
7
|
-
|
|
8
|
-
describe("MarkdownParser", () => {
|
|
9
|
-
describe("escapeHtml", () => {
|
|
10
|
-
it("should escape HTML entities", () => {
|
|
11
|
-
expect(escapeHtml('<script>alert("xss")</script>')).toBe("<script>alert("xss")</script>");
|
|
12
|
-
expect(escapeHtml("&<>\"'`")).toBe("&<>"'`");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("should handle null and undefined", () => {
|
|
16
|
-
expect(escapeHtml(null)).toBe("");
|
|
17
|
-
expect(escapeHtml(undefined)).toBe("");
|
|
18
|
-
expect(escapeHtml("")).toBe("");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("should preserve safe text", () => {
|
|
22
|
-
expect(escapeHtml("Hello World!")).toBe("Hello World!");
|
|
23
|
-
expect(escapeHtml("123 + 456 = 579")).toBe("123 + 456 = 579");
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe("parseMarkdown", () => {
|
|
28
|
-
it("should handle null and undefined content", () => {
|
|
29
|
-
expect(parseMarkdown(null)).toEqual([]);
|
|
30
|
-
expect(parseMarkdown(undefined)).toEqual([]);
|
|
31
|
-
expect(parseMarkdown("")).toEqual([]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("should parse simple markdown text", () => {
|
|
35
|
-
const result = parseMarkdown("Hello **world**!");
|
|
36
|
-
expect(result).toHaveLength(1);
|
|
37
|
-
expect(result[0]).toContain("<strong>world</strong>");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("should handle code blocks as single block", () => {
|
|
41
|
-
const codeBlock = '```javascript\nconsole.log("hello");\n```';
|
|
42
|
-
const result = parseMarkdown(codeBlock);
|
|
43
|
-
expect(result).toHaveLength(1);
|
|
44
|
-
expect(result[0]).toContain("console.log");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("should handle tables as single block", () => {
|
|
48
|
-
const table = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
|
|
49
|
-
const result = parseMarkdown(table);
|
|
50
|
-
expect(result).toHaveLength(1);
|
|
51
|
-
expect(result[0]).toContain("<table>");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("should split text by double newlines", () => {
|
|
55
|
-
const text = "Paragraph 1\n\nParagraph 2\n\nParagraph 3";
|
|
56
|
-
const result = parseMarkdown(text);
|
|
57
|
-
expect(result).toHaveLength(3);
|
|
58
|
-
expect(result[0]).toContain("Paragraph 1");
|
|
59
|
-
expect(result[1]).toContain("Paragraph 2");
|
|
60
|
-
expect(result[2]).toContain("Paragraph 3");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("Link Security Tests", () => {
|
|
64
|
-
it("should sanitize javascript: URLs", () => {
|
|
65
|
-
const maliciousLink = '[Click me](javascript:alert("xss"))';
|
|
66
|
-
const result = parseMarkdown(maliciousLink);
|
|
67
|
-
expect(result[0]).not.toContain("javascript:");
|
|
68
|
-
// Should render as text or empty href
|
|
69
|
-
expect(result[0]).toContain("Click me");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("should sanitize data: URLs", () => {
|
|
73
|
-
const dataUrl = '[Click me](data:text/html,<script>alert("xss")</script>)';
|
|
74
|
-
const result = parseMarkdown(dataUrl);
|
|
75
|
-
expect(result[0]).not.toContain("data:text/html");
|
|
76
|
-
expect(result[0]).toContain("Click me");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("should allow https URLs", () => {
|
|
80
|
-
const httpsLink = "[Safe link](https://example.com)";
|
|
81
|
-
const result = parseMarkdown(httpsLink);
|
|
82
|
-
expect(result[0]).toContain('href="https://example.com"');
|
|
83
|
-
expect(result[0]).toContain('target="_blank"');
|
|
84
|
-
expect(result[0]).toContain('rel="noopener noreferrer"');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("should allow http URLs", () => {
|
|
88
|
-
const httpLink = "[HTTP link](http://example.com)";
|
|
89
|
-
const result = parseMarkdown(httpLink);
|
|
90
|
-
expect(result[0]).toContain('href="http://example.com"');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("should allow mailto URLs", () => {
|
|
94
|
-
const mailtoLink = "[Email me](mailto:test@example.com)";
|
|
95
|
-
const result = parseMarkdown(mailtoLink);
|
|
96
|
-
expect(result[0]).toContain('href="mailto:test@example.com"');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("should handle protocol-relative URLs", () => {
|
|
100
|
-
const protocolRelative = "[Link](//example.com)";
|
|
101
|
-
const result = parseMarkdown(protocolRelative);
|
|
102
|
-
expect(result[0]).toContain('href="https://example.com"');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("should allow relative URLs", () => {
|
|
106
|
-
const relativeLink = "[Relative](/path/to/page)";
|
|
107
|
-
const result = parseMarkdown(relativeLink);
|
|
108
|
-
expect(result[0]).toContain('href="/path/to/page"');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("should escape link titles and text", () => {
|
|
112
|
-
const linkWithQuotes = '[Link with "quotes"](https://example.com "Title with \'quotes\'")';
|
|
113
|
-
const result = parseMarkdown(linkWithQuotes);
|
|
114
|
-
expect(result[0]).toContain('title="Title with 'quotes'"');
|
|
115
|
-
expect(result[0]).toContain("Link with "quotes"");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("should handle XSS attempts in link text", () => {
|
|
119
|
-
const xssText = '[<script>alert("xss")</script>](https://example.com)';
|
|
120
|
-
const result = parseMarkdown(xssText);
|
|
121
|
-
expect(result[0]).not.toContain("<script>");
|
|
122
|
-
expect(result[0]).toContain("<script>");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("should handle XSS attempts in link titles", () => {
|
|
126
|
-
const xssTitle = '[Link](https://example.com "Title<script>alert(\\"xss\\")</script>")';
|
|
127
|
-
const result = parseMarkdown(xssTitle);
|
|
128
|
-
expect(result[0]).not.toContain("<script>");
|
|
129
|
-
expect(result[0]).toContain("<script>");
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe("Math Formula Tests", () => {
|
|
134
|
-
it("should render inline math formulas", () => {
|
|
135
|
-
const mathFormula = "The equation is $x^2 + y^2 = z^2$";
|
|
136
|
-
const result = parseMarkdown(mathFormula);
|
|
137
|
-
expect(result[0]).toContain("katex");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("should render block math formulas", () => {
|
|
141
|
-
const blockMath = "$$\\sum_{i=1}^{n} x_i$$";
|
|
142
|
-
const result = parseMarkdown(blockMath);
|
|
143
|
-
expect(result[0]).toContain("katex");
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe("Code Highlighting Tests", () => {
|
|
148
|
-
it("should highlight code with language specified", () => {
|
|
149
|
-
const code = "```javascript\nconst x = 5;\n```";
|
|
150
|
-
const result = parseMarkdown(code);
|
|
151
|
-
expect(result[0]).toContain("hljs");
|
|
152
|
-
expect(result[0]).toContain("code-data");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("should handle code without language", () => {
|
|
156
|
-
const code = "```\nsome code\n```";
|
|
157
|
-
const result = parseMarkdown(code);
|
|
158
|
-
// Code blocks without language don't get hljs classes applied
|
|
159
|
-
expect(result[0]).toContain("<pre><code>");
|
|
160
|
-
expect(result[0]).toContain("some code");
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe("parseChatMessages", () => {
|
|
166
|
-
const mockMessage: Message = {
|
|
167
|
-
id: "1",
|
|
168
|
-
chatId: "chat1",
|
|
169
|
-
content: "Hello **world**!",
|
|
170
|
-
role: MessageRole.USER,
|
|
171
|
-
createdAt: new Date().toISOString(),
|
|
172
|
-
updatedAt: new Date().toISOString(),
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
it("should handle empty messages array", () => {
|
|
176
|
-
expect(parseChatMessages([])).toEqual([]);
|
|
177
|
-
expect(parseChatMessages()).toEqual([]);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("should parse USER and ASSISTANT messages as markdown", () => {
|
|
181
|
-
const messages: Message[] = [
|
|
182
|
-
{ ...mockMessage, role: MessageRole.USER },
|
|
183
|
-
{ ...mockMessage, role: MessageRole.ASSISTANT, id: "2" },
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
const result = parseChatMessages(messages);
|
|
187
|
-
|
|
188
|
-
expect(result).toHaveLength(2);
|
|
189
|
-
expect(result[0].html).toBeDefined();
|
|
190
|
-
expect(result[1].html).toBeDefined();
|
|
191
|
-
expect(result[0].html![0]).toContain("<strong>world</strong>");
|
|
192
|
-
expect(result[1].html![0]).toContain("<strong>world</strong>");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("should escape non-USER/ASSISTANT messages", () => {
|
|
196
|
-
const systemMessage: Message = {
|
|
197
|
-
...mockMessage,
|
|
198
|
-
role: MessageRole.ERROR,
|
|
199
|
-
content: '<script>alert("xss")</script>',
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const result = parseChatMessages([systemMessage]);
|
|
203
|
-
|
|
204
|
-
expect(result).toHaveLength(1);
|
|
205
|
-
expect(result[0].html![0]).toBe("<script>alert("xss")</script>");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("should handle linked messages", () => {
|
|
209
|
-
const messageWithLinked: Message = {
|
|
210
|
-
...mockMessage,
|
|
211
|
-
linkedMessages: [
|
|
212
|
-
{ ...mockMessage, id: "2", role: MessageRole.ASSISTANT, content: "Response **bold**" },
|
|
213
|
-
{ ...mockMessage, id: "3", role: MessageRole.SYSTEM, content: '<script>alert("xss")</script>' },
|
|
214
|
-
],
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const result = parseChatMessages([messageWithLinked]);
|
|
218
|
-
|
|
219
|
-
expect(result).toHaveLength(1);
|
|
220
|
-
expect(result[0].linkedMessages).toHaveLength(2);
|
|
221
|
-
expect(result[0].linkedMessages![0].html![0]).toContain("<strong>bold</strong>");
|
|
222
|
-
expect(result[0].linkedMessages![1].html![0]).toContain("<script>");
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("should handle malicious links in chat messages", () => {
|
|
226
|
-
const maliciousMessage: Message = {
|
|
227
|
-
...mockMessage,
|
|
228
|
-
content: '[Click here](javascript:alert("xss")) for a "surprise"',
|
|
229
|
-
role: MessageRole.USER,
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const result = parseChatMessages([maliciousMessage]);
|
|
233
|
-
|
|
234
|
-
expect(result[0].html![0]).not.toContain("javascript:");
|
|
235
|
-
expect(result[0].html![0]).toContain("Click here");
|
|
236
|
-
expect(result[0].html![0]).toContain(""surprise"");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("should preserve message metadata", () => {
|
|
240
|
-
const result = parseChatMessages([mockMessage]);
|
|
241
|
-
|
|
242
|
-
expect(result[0].id).toBe(mockMessage.id);
|
|
243
|
-
expect(result[0].role).toBe(mockMessage.role);
|
|
244
|
-
expect(result[0].createdAt).toBe(mockMessage.createdAt);
|
|
245
|
-
expect(result[0].updatedAt).toBe(mockMessage.updatedAt);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe("HTML Injection Prevention", () => {
|
|
250
|
-
it("should prevent HTML injection in regular text", () => {
|
|
251
|
-
const maliciousText = 'Hello <img src="x" onerror="alert(\'xss\')">';
|
|
252
|
-
const result = parseMarkdown(maliciousText);
|
|
253
|
-
// HTML should be escaped, but onerror= might still appear in escaped form
|
|
254
|
-
expect(result[0]).toContain("<img");
|
|
255
|
-
expect(result[0]).toContain(""alert");
|
|
256
|
-
// Should not contain unescaped HTML
|
|
257
|
-
expect(result[0]).not.toContain('<img src="x"');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("should prevent HTML injection in code blocks", () => {
|
|
261
|
-
const maliciousCode = '```html\n<script>alert("xss")</script>\n```';
|
|
262
|
-
const result = parseMarkdown(maliciousCode);
|
|
263
|
-
// Code in code blocks should be syntax highlighted but not executed
|
|
264
|
-
// HTML syntax highlighting will show tags but they're still safe
|
|
265
|
-
expect(result[0]).toContain("hljs-tag");
|
|
266
|
-
expect(result[0]).toContain("script");
|
|
267
|
-
// Should not contain executable script tag
|
|
268
|
-
expect(result[0]).not.toContain('<script>alert("xss")</script>');
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("should handle mixed content safely", () => {
|
|
272
|
-
const mixedContent = `
|
|
273
|
-
# Header with <script>alert("xss")</script>
|
|
274
|
-
|
|
275
|
-
[Malicious link](javascript:void(0)) and normal [safe link](https://example.com)
|
|
276
|
-
|
|
277
|
-
\`\`\`javascript
|
|
278
|
-
// This <script> should be highlighted but not executed
|
|
279
|
-
console.log("<script>alert('safe')</script>");
|
|
280
|
-
\`\`\`
|
|
281
|
-
`;
|
|
282
|
-
|
|
283
|
-
const result = parseMarkdown(mixedContent);
|
|
284
|
-
expect(result[0]).not.toContain("javascript:void(0)");
|
|
285
|
-
expect(result[0]).toContain("https://example.com");
|
|
286
|
-
expect(result[0]).toContain("<script>alert('safe')</script>");
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// Test utilities to access internal functions
|
|
2
|
-
// This file helps test non-exported functions from MarkdownParser
|
|
3
|
-
|
|
4
|
-
import { parseMarkdown } from "../markdown.parser";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Test the sanitizeUrl function indirectly by checking link rendering
|
|
8
|
-
*/
|
|
9
|
-
export function testSanitizeUrl(url: string): string {
|
|
10
|
-
const testMarkdown = `[test](${url})`;
|
|
11
|
-
const result = parseMarkdown(testMarkdown);
|
|
12
|
-
|
|
13
|
-
// Extract href value from the rendered HTML
|
|
14
|
-
const match = result[0].match(/href="([^"]*)"/);
|
|
15
|
-
return match ? match[1] : "";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Test if a URL gets blocked (returns empty href)
|
|
20
|
-
*/
|
|
21
|
-
export function testUrlBlocked(url: string): boolean {
|
|
22
|
-
return testSanitizeUrl(url) === "";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Test if a URL gets allowed and properly escaped
|
|
27
|
-
*/
|
|
28
|
-
export function testUrlAllowed(url: string, expectedUrl?: string): boolean {
|
|
29
|
-
const result = testSanitizeUrl(url);
|
|
30
|
-
return result !== "" && (expectedUrl ? result === expectedUrl : true);
|
|
31
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { testSanitizeUrl, testUrlBlocked, testUrlAllowed } from "./markdown.parser.testUtils";
|
|
2
|
-
|
|
3
|
-
describe("sanitizeUrl (internal function tests)", () => {
|
|
4
|
-
describe("Dangerous URL Blocking", () => {
|
|
5
|
-
it("should block javascript: URLs", () => {
|
|
6
|
-
expect(testUrlBlocked('javascript:alert("xss")')).toBe(true);
|
|
7
|
-
expect(testUrlBlocked("JavaScript:void(0)")).toBe(true);
|
|
8
|
-
expect(testUrlBlocked("JAVASCRIPT:alert(1)")).toBe(true);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("should block data: URLs", () => {
|
|
12
|
-
expect(testUrlBlocked('data:text/html,<script>alert("xss")</script>')).toBe(true);
|
|
13
|
-
expect(testUrlBlocked("data:image/svg+xml,<svg onload=alert(1)>")).toBe(true);
|
|
14
|
-
expect(testUrlBlocked("DATA:text/plain,malicious")).toBe(true);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("should block other dangerous protocols", () => {
|
|
18
|
-
expect(testUrlBlocked('vbscript:msgbox("xss")')).toBe(true);
|
|
19
|
-
expect(testUrlBlocked("file:///etc/passwd")).toBe(true);
|
|
20
|
-
expect(testUrlBlocked("ftp://malicious.com")).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("should handle empty and null-like values", () => {
|
|
24
|
-
expect(testSanitizeUrl("")).toBe("");
|
|
25
|
-
expect(testSanitizeUrl(" ")).toBe("");
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("Safe URL Allowing", () => {
|
|
30
|
-
it("should allow https URLs", () => {
|
|
31
|
-
expect(testUrlAllowed("https://example.com")).toBe(true);
|
|
32
|
-
expect(testSanitizeUrl("https://example.com")).toBe("https://example.com");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("should allow http URLs", () => {
|
|
36
|
-
expect(testUrlAllowed("http://example.com")).toBe(true);
|
|
37
|
-
expect(testSanitizeUrl("http://example.com")).toBe("http://example.com");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("should allow mailto URLs", () => {
|
|
41
|
-
expect(testUrlAllowed("mailto:test@example.com")).toBe(true);
|
|
42
|
-
expect(testSanitizeUrl("mailto:test@example.com")).toBe("mailto:test@example.com");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("should handle protocol-relative URLs", () => {
|
|
46
|
-
expect(testSanitizeUrl("//example.com")).toBe("https://example.com");
|
|
47
|
-
expect(testSanitizeUrl("//cdn.example.com/file.css")).toBe("https://cdn.example.com/file.css");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("should allow relative URLs", () => {
|
|
51
|
-
expect(testUrlAllowed("/path/to/page")).toBe(true);
|
|
52
|
-
expect(testUrlAllowed("./relative/path")).toBe(true);
|
|
53
|
-
expect(testUrlAllowed("../parent/path")).toBe(true);
|
|
54
|
-
expect(testSanitizeUrl("/api/endpoint")).toBe("/api/endpoint");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("URL Escaping", () => {
|
|
59
|
-
it("should escape HTML entities in URLs", () => {
|
|
60
|
-
const urlWithEntities = "https://example.com/search?q=<script>&=value";
|
|
61
|
-
const result = testSanitizeUrl(urlWithEntities);
|
|
62
|
-
expect(result).toContain("<script>");
|
|
63
|
-
expect(result).toContain("&amp=value");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("should handle URLs with quotes", () => {
|
|
67
|
-
const urlWithQuotes = "https://example.com/path?param=\"value\"&other='test'";
|
|
68
|
-
const result = testSanitizeUrl(urlWithQuotes);
|
|
69
|
-
expect(result).toContain(""value"");
|
|
70
|
-
expect(result).toContain("'test'");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("should preserve query parameters safely", () => {
|
|
74
|
-
const complexUrl = "https://api.example.com/search?q=test&sort=date&filter[]=category";
|
|
75
|
-
const result = testSanitizeUrl(complexUrl);
|
|
76
|
-
// Ampersands get escaped as expected
|
|
77
|
-
expect(result).toBe("https://api.example.com/search?q=test&sort=date&filter[]=category");
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("Edge Cases", () => {
|
|
82
|
-
it("should handle URLs with whitespace", () => {
|
|
83
|
-
expect(testSanitizeUrl(" https://example.com ")).toBe("https://example.com");
|
|
84
|
-
// Note: literal \n characters in string, not actual newlines
|
|
85
|
-
expect(testSanitizeUrl("https://example.com")).toBe("https://example.com");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("should handle mixed case protocols", () => {
|
|
89
|
-
expect(testSanitizeUrl("HTTPS://EXAMPLE.COM")).toBe("HTTPS://EXAMPLE.COM");
|
|
90
|
-
expect(testSanitizeUrl("Http://example.com")).toBe("Http://example.com");
|
|
91
|
-
expect(testSanitizeUrl("MailTo:test@example.com")).toBe("MailTo:test@example.com");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("should handle URLs without protocols correctly", () => {
|
|
95
|
-
expect(testUrlAllowed("example.com")).toBe(true);
|
|
96
|
-
expect(testUrlAllowed("www.example.com/path")).toBe(true);
|
|
97
|
-
expect(testSanitizeUrl("example.com")).toBe("example.com");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("should handle domain names that contain suspicious words", () => {
|
|
101
|
-
// These are actually valid domain names, just happen to contain suspicious words
|
|
102
|
-
// They should be allowed as they're not actually protocols
|
|
103
|
-
expect(testUrlAllowed("javascript.com:8080")).toBe(true);
|
|
104
|
-
expect(testUrlAllowed("data.evil.com:3000")).toBe(true);
|
|
105
|
-
|
|
106
|
-
// But actual protocols should still be blocked
|
|
107
|
-
expect(testUrlBlocked("javascript:alert(1)")).toBe(true);
|
|
108
|
-
expect(testUrlBlocked("data:text/html,<script>")).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("Security Edge Cases", () => {
|
|
113
|
-
it("should handle URL encoding attempts", () => {
|
|
114
|
-
expect(testUrlBlocked("javascript%3Aalert(1)")).toBe(true);
|
|
115
|
-
expect(testUrlBlocked("data%3Atext/html,<script>")).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("should handle various protocol separators", () => {
|
|
119
|
-
expect(testUrlBlocked("javascript://alert(1)")).toBe(true);
|
|
120
|
-
expect(testUrlBlocked("javascript:\\\\alert(1)")).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("should handle attempts to bypass with special characters", () => {
|
|
124
|
-
// These specific attempts with literal strings may not be blocked by our simple regex
|
|
125
|
-
// but the basic javascript: detection should work
|
|
126
|
-
expect(testUrlBlocked("javascript:alert(1)")).toBe(true);
|
|
127
|
-
expect(testUrlBlocked("JAVASCRIPT:alert(1)")).toBe(true);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
package/src/lib/assert.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// Pure assertion tests whether a value is truthy, as determined by !!value.
|
|
2
|
-
export function ok<T>(value: T, message?: string | Error): asserts value {
|
|
3
|
-
if (!value) {
|
|
4
|
-
if (message instanceof Error) {
|
|
5
|
-
throw message;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
throw new Error(message || "No value argument passed to `assert.ok()`");
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function notEmpty<T>(value: T | undefined | null): value is T {
|
|
13
|
-
return value != undefined;
|
|
14
|
-
}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import hljs from "highlight.js";
|
|
2
|
-
import { Marked, Renderer } from "marked";
|
|
3
|
-
import { markedHighlight } from "marked-highlight";
|
|
4
|
-
import markedKatex from "marked-katex-extension";
|
|
5
|
-
|
|
6
|
-
import { Message, MessageRole } from "@/core/message";
|
|
7
|
-
|
|
8
|
-
// Template to store original (unformatted) code to copy it
|
|
9
|
-
const CodeDataTemplate = `<span class="code-data" data-code="<CODE>" data-lang="<LANG>"></span>`;
|
|
10
|
-
|
|
11
|
-
const marked = new Marked(
|
|
12
|
-
// code highlighting
|
|
13
|
-
markedHighlight({
|
|
14
|
-
emptyLangClass: "hljs plaintext",
|
|
15
|
-
langPrefix: "hljs ",
|
|
16
|
-
highlight(code: string, lang: string) {
|
|
17
|
-
const language = hljs.getLanguage(lang) ? lang : "plaintext";
|
|
18
|
-
const formattedCode: string = hljs.highlight(code?.trim(), { language }).value;
|
|
19
|
-
|
|
20
|
-
if (lang) {
|
|
21
|
-
return (
|
|
22
|
-
CodeDataTemplate.replaceAll("<LANG>", language).replaceAll("<CODE>", encodeURIComponent(code)) +
|
|
23
|
-
formattedCode?.trim()
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return formattedCode;
|
|
28
|
-
},
|
|
29
|
-
}),
|
|
30
|
-
|
|
31
|
-
// MatJAX formulas processing
|
|
32
|
-
// example prompt with Claude V3 Haiku: "show me some example math equations like pythagoras, also add some example of an addition of 2 matrixes"
|
|
33
|
-
// example prompt with Claude V3 Haiku: "show a proof of x is smaller y"
|
|
34
|
-
markedKatex({
|
|
35
|
-
displayMode: false,
|
|
36
|
-
throwOnError: false,
|
|
37
|
-
output: "html",
|
|
38
|
-
}),
|
|
39
|
-
{ silent: true, gfm: true, breaks: true }
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const markedSimple = new Marked(
|
|
43
|
-
markedHighlight({
|
|
44
|
-
emptyLangClass: "hljs plaintext",
|
|
45
|
-
langPrefix: "hljs ",
|
|
46
|
-
highlight(code: string, lang: string) {
|
|
47
|
-
const language = hljs.getLanguage(lang) ? lang : "plaintext";
|
|
48
|
-
return hljs.highlight(code?.trim(), { language }).value;
|
|
49
|
-
},
|
|
50
|
-
}),
|
|
51
|
-
{ silent: true, breaks: true, gfm: true }
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const renderer = new Renderer();
|
|
55
|
-
renderer.html = ({ text }: { text: string }) => {
|
|
56
|
-
return escapeHtml(text);
|
|
57
|
-
};
|
|
58
|
-
renderer.link = ({ href, title, text }) => {
|
|
59
|
-
// Sanitize URL to prevent XSS attacks
|
|
60
|
-
const url = sanitizeUrl(href);
|
|
61
|
-
return `<a target="_blank" rel="noopener noreferrer" href="${url}" title="${escapeHtml(title) || ""}">${escapeHtml(text)}</a>`;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export function normalizeMatJAX(input: string): string {
|
|
65
|
-
return input
|
|
66
|
-
? input
|
|
67
|
-
.replace(/\\\(([\s\S]+?)\\\)/g, (_, expr) => `$${expr}$`)
|
|
68
|
-
// Block math: \[ ... \] → $$ ... $$ (on newlines for KaTeX block mode)
|
|
69
|
-
.replace(/\\\[([\s\S]+?)\\\]/g, (_, expr) => `\n$$${expr}$$\n`)
|
|
70
|
-
: "";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Parse markdown to html blocks
|
|
75
|
-
* @param content Raw markdown
|
|
76
|
-
* @returns Array for formatted HTML blocks to be rendered
|
|
77
|
-
*/
|
|
78
|
-
export function parseMarkdown(content?: string | null, simple = false): string[] {
|
|
79
|
-
if (!content) return [];
|
|
80
|
-
|
|
81
|
-
if (simple) {
|
|
82
|
-
return [markedSimple.parse(content, { renderer }) as string];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
content = normalizeMatJAX(content);
|
|
86
|
-
// process complex code blocks, tables as one block
|
|
87
|
-
if (content.match(/(```)|(\|---)/)) {
|
|
88
|
-
return [marked.parse(content, { renderer }) as string];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// split large texts
|
|
92
|
-
const parts = content
|
|
93
|
-
.split(/(\r)?\n(\r)?\n/g)
|
|
94
|
-
.filter(s => Boolean(s))
|
|
95
|
-
.map(s => s + "\n\n");
|
|
96
|
-
|
|
97
|
-
return parts.map(part => marked.parse(part, { renderer }) as string);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function parseChatMessages(messages: Message[] = []): Message[] {
|
|
101
|
-
const parsedMessages: Message[] = Array<Message>(messages.length);
|
|
102
|
-
for (let i = 0; i < messages.length; i++) {
|
|
103
|
-
const message = messages[i];
|
|
104
|
-
const html =
|
|
105
|
-
message.role === MessageRole.ASSISTANT || message.role === MessageRole.USER
|
|
106
|
-
? parseMarkdown(message.content)
|
|
107
|
-
: [escapeHtml(message.content) || ""];
|
|
108
|
-
|
|
109
|
-
let linkedMessages = message.linkedMessages;
|
|
110
|
-
if (linkedMessages) {
|
|
111
|
-
const linkedMessagesParsed = Array<Message>(linkedMessages.length);
|
|
112
|
-
for (let ndx = 0; ndx < linkedMessages.length; ndx++) {
|
|
113
|
-
const linkedMessage = linkedMessages[ndx];
|
|
114
|
-
if (linkedMessage.role === MessageRole.ASSISTANT || linkedMessage.role === MessageRole.USER) {
|
|
115
|
-
linkedMessagesParsed[ndx] = {
|
|
116
|
-
...linkedMessage,
|
|
117
|
-
html: parseMarkdown(linkedMessage.content),
|
|
118
|
-
};
|
|
119
|
-
} else {
|
|
120
|
-
linkedMessagesParsed[ndx] = {
|
|
121
|
-
...linkedMessage,
|
|
122
|
-
html: [escapeHtml(linkedMessage.content) || ""],
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
linkedMessages = linkedMessagesParsed;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
parsedMessages[i] = {
|
|
131
|
-
...message,
|
|
132
|
-
linkedMessages,
|
|
133
|
-
html,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return parsedMessages;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const ESCAPE_HTML_ENTITIES: { [key: string]: string } = {
|
|
141
|
-
"&": "&",
|
|
142
|
-
"<": "<",
|
|
143
|
-
">": ">",
|
|
144
|
-
'"': """,
|
|
145
|
-
"'": "'",
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
export function escapeHtml(text?: string | null): string {
|
|
149
|
-
if (!text) return "";
|
|
150
|
-
return text.replace(/[&<>"']/g, match => ESCAPE_HTML_ENTITIES[match] || match);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Sanitize URL to prevent XSS attacks
|
|
155
|
-
* Only allows http, https, and mailto protocols
|
|
156
|
-
*/
|
|
157
|
-
function sanitizeUrl(url?: string | null): string {
|
|
158
|
-
if (!url) return "";
|
|
159
|
-
|
|
160
|
-
// Remove any whitespace and decode basic URL encoding for protocol detection
|
|
161
|
-
const trimmedUrl = url.trim();
|
|
162
|
-
const decodedUrl = decodeURIComponent(trimmedUrl).toLowerCase();
|
|
163
|
-
|
|
164
|
-
const allowedProtocols = /^(https?:\/\/|mailto:)/i;
|
|
165
|
-
if (allowedProtocols.test(trimmedUrl)) {
|
|
166
|
-
return escapeHtml(trimmedUrl);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// If it starts with //, assume https
|
|
170
|
-
if (trimmedUrl.startsWith("//")) {
|
|
171
|
-
return escapeHtml(`https:${trimmedUrl}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const dangerousProtocols = /^(javascript|data|vbscript|file|ftp):/i;
|
|
175
|
-
if (dangerousProtocols.test(decodedUrl)) {
|
|
176
|
-
return "";
|
|
177
|
-
}
|
|
178
|
-
if (decodedUrl.includes("javascript:") || decodedUrl.includes("data:") || decodedUrl.includes("vbscript:")) {
|
|
179
|
-
return "";
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// If it looks like a relative path or doesn't have a protocol, allow it
|
|
183
|
-
if (trimmedUrl.startsWith("/") || !trimmedUrl.includes("://")) {
|
|
184
|
-
return escapeHtml(trimmedUrl);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Block any other unknown protocols
|
|
188
|
-
return "";
|
|
189
|
-
}
|
package/src/setupTests.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import "@testing-library/jest-dom";
|
package/src/types/scss.d.ts
DELETED