detached 7d98a0f

feat: implement password validation and comprehensive security testing framework

commit
7d98a0ff2312fbb9fcc032b2153a1e5bd16e1dac
author
Kit · · 2025-07-03 19:55 UTC
parent
5f5cd68
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
4 files · +818 -0
A src/lib/components/password-strength-indicator.svelte +165 -0
@@ -0,0 +1,165 @@
1+<script lang="ts">
2+ import { validatePasswordStrength, type PasswordConstraints, DEFAULT_PASSWORD_CONSTRAINTS } from '../validation/password';
3+
4+ interface $$Props {
5+ password: string;
6+ constraints?: PasswordConstraints;
7+ showDetails?: boolean;
8+ }
9+
10+ let {
11+ password = '',
12+ constraints = DEFAULT_PASSWORD_CONSTRAINTS,
13+ showDetails = true
14+ } = $props();
15+
16+ // Reactive validation result
17+ let validation = $derived(validatePasswordStrength(password, constraints));
18+
19+ // Get strength color and label
20+ function getStrengthInfo(strength: number) {
21+ if (strength < 30) return { color: '#ff4444', label: 'Weak', class: 'weak' };
22+ if (strength < 60) return { color: '#ffaa00', label: 'Fair', class: 'fair' };
23+ if (strength < 80) return { color: '#44aa44', label: 'Good', class: 'good' };
24+ return { color: '#00aa00', label: 'Strong', class: 'strong' };
25+ }
26+
27+ let strengthInfo = $derived(getStrengthInfo(validation.strength));
28+</script>
29+
30+<div class="password-strength-indicator">
31+ {#if password.length > 0}
32+ <!-- Strength bar -->
33+ <div class="strength-bar">
34+ <div class="strength-bar-bg">
35+ <div
36+ class="strength-bar-fill {strengthInfo.class}"
37+ style="width: {validation.strength}%; background-color: {strengthInfo.color};"
38+ ></div>
39+ </div>
40+ <span class="strength-label {strengthInfo.class}">{strengthInfo.label}</span>
41+ </div>
42+
43+ {#if showDetails}
44+ <!-- Requirements checklist -->
45+ <div class="requirements-list">
46+ {#each validation.requirements as requirement}
47+ <div class="requirement {requirement.met ? 'met' : 'unmet'}">
48+ <span class="requirement-icon">
49+ {requirement.met ? '✓' : '✗'}
50+ </span>
51+ <span class="requirement-text">{requirement.message}</span>
52+ </div>
53+ {/each}
54+ </div>
55+
56+ <!-- Error messages -->
57+ {#if validation.errors.length > 0}
58+ <div class="error-messages">
59+ {#each validation.errors as error}
60+ <div class="error-message">{error}</div>
61+ {/each}
62+ </div>
63+ {/if}
64+ {/if}
65+ {/if}
66+</div>
67+
68+<style>
69+ .password-strength-indicator {
70+ margin-top: 8px;
71+ font-size: 0.875rem;
72+ }
73+
74+ .strength-bar {
75+ display: flex;
76+ align-items: center;
77+ gap: 8px;
78+ margin-bottom: 8px;
79+ }
80+
81+ .strength-bar-bg {
82+ flex: 1;
83+ height: 6px;
84+ background-color: #ddd;
85+ border-radius: 3px;
86+ overflow: hidden;
87+ }
88+
89+ .strength-bar-fill {
90+ height: 100%;
91+ transition: width 0.3s ease, background-color 0.3s ease;
92+ border-radius: 3px;
93+ }
94+
95+ .strength-label {
96+ min-width: 50px;
97+ font-weight: bold;
98+ font-size: 0.75rem;
99+ }
100+
101+ .strength-label.weak { color: #ff4444; }
102+ .strength-label.fair { color: #ffaa00; }
103+ .strength-label.good { color: #44aa44; }
104+ .strength-label.strong { color: #00aa00; }
105+
106+ .requirements-list {
107+ display: flex;
108+ flex-direction: column;
109+ gap: 4px;
110+ margin-bottom: 8px;
111+ }
112+
113+ .requirement {
114+ display: flex;
115+ align-items: center;
116+ gap: 6px;
117+ font-size: 0.75rem;
118+ }
119+
120+ .requirement-icon {
121+ width: 12px;
122+ text-align: center;
123+ font-weight: bold;
124+ }
125+
126+ .requirement.met {
127+ color: #00aa00;
128+ }
129+
130+ .requirement.met .requirement-icon {
131+ color: #00aa00;
132+ }
133+
134+ .requirement.unmet {
135+ color: #888;
136+ }
137+
138+ .requirement.unmet .requirement-icon {
139+ color: #ff4444;
140+ }
141+
142+ .error-messages {
143+ display: flex;
144+ flex-direction: column;
145+ gap: 4px;
146+ }
147+
148+ .error-message {
149+ color: #ff4444;
150+ font-size: 0.75rem;
151+ }
152+
153+ /* Compact mode styles */
154+ :global(.compact) .password-strength-indicator {
155+ font-size: 0.75rem;
156+ }
157+
158+ :global(.compact) .requirements-list {
159+ display: none;
160+ }
161+
162+ :global(.compact) .strength-bar {
163+ margin-bottom: 4px;
164+ }
165+</style>⏎̸
@@ -0,0 +1,165 @@
1+<script lang="ts">
2+ import { validatePasswordStrength, type PasswordConstraints, DEFAULT_PASSWORD_CONSTRAINTS } from '../validation/password';
3+
4+ interface $$Props {
5+ password: string;
6+ constraints?: PasswordConstraints;
7+ showDetails?: boolean;
8+ }
9+
10+ let {
11+ password = '',
12+ constraints = DEFAULT_PASSWORD_CONSTRAINTS,
13+ showDetails = true
14+ } = $props();
15+
16+ // Reactive validation result
17+ let validation = $derived(validatePasswordStrength(password, constraints));
18+
19+ // Get strength color and label
20+ function getStrengthInfo(strength: number) {
21+ if (strength < 30) return { color: '#ff4444', label: 'Weak', class: 'weak' };
22+ if (strength < 60) return { color: '#ffaa00', label: 'Fair', class: 'fair' };
23+ if (strength < 80) return { color: '#44aa44', label: 'Good', class: 'good' };
24+ return { color: '#00aa00', label: 'Strong', class: 'strong' };
25+ }
26+
27+ let strengthInfo = $derived(getStrengthInfo(validation.strength));
28+</script>
29+
30+<div class="password-strength-indicator">
31+ {#if password.length > 0}
32+ <!-- Strength bar -->
33+ <div class="strength-bar">
34+ <div class="strength-bar-bg">
35+ <div
36+ class="strength-bar-fill {strengthInfo.class}"
37+ style="width: {validation.strength}%; background-color: {strengthInfo.color};"
38+ ></div>
39+ </div>
40+ <span class="strength-label {strengthInfo.class}">{strengthInfo.label}</span>
41+ </div>
42+
43+ {#if showDetails}
44+ <!-- Requirements checklist -->
45+ <div class="requirements-list">
46+ {#each validation.requirements as requirement}
47+ <div class="requirement {requirement.met ? 'met' : 'unmet'}">
48+ <span class="requirement-icon">
49+ {requirement.met ? '✓' : '✗'}
50+ </span>
51+ <span class="requirement-text">{requirement.message}</span>
52+ </div>
53+ {/each}
54+ </div>
55+
56+ <!-- Error messages -->
57+ {#if validation.errors.length > 0}
58+ <div class="error-messages">
59+ {#each validation.errors as error}
60+ <div class="error-message">{error}</div>
61+ {/each}
62+ </div>
63+ {/if}
64+ {/if}
65+ {/if}
66+</div>
67+
68+<style>
69+ .password-strength-indicator {
70+ margin-top: 8px;
71+ font-size: 0.875rem;
72+ }
73+
74+ .strength-bar {
75+ display: flex;
76+ align-items: center;
77+ gap: 8px;
78+ margin-bottom: 8px;
79+ }
80+
81+ .strength-bar-bg {
82+ flex: 1;
83+ height: 6px;
84+ background-color: #ddd;
85+ border-radius: 3px;
86+ overflow: hidden;
87+ }
88+
89+ .strength-bar-fill {
90+ height: 100%;
91+ transition: width 0.3s ease, background-color 0.3s ease;
92+ border-radius: 3px;
93+ }
94+
95+ .strength-label {
96+ min-width: 50px;
97+ font-weight: bold;
98+ font-size: 0.75rem;
99+ }
100+
101+ .strength-label.weak { color: #ff4444; }
102+ .strength-label.fair { color: #ffaa00; }
103+ .strength-label.good { color: #44aa44; }
104+ .strength-label.strong { color: #00aa00; }
105+
106+ .requirements-list {
107+ display: flex;
108+ flex-direction: column;
109+ gap: 4px;
110+ margin-bottom: 8px;
111+ }
112+
113+ .requirement {
114+ display: flex;
115+ align-items: center;
116+ gap: 6px;
117+ font-size: 0.75rem;
118+ }
119+
120+ .requirement-icon {
121+ width: 12px;
122+ text-align: center;
123+ font-weight: bold;
124+ }
125+
126+ .requirement.met {
127+ color: #00aa00;
128+ }
129+
130+ .requirement.met .requirement-icon {
131+ color: #00aa00;
132+ }
133+
134+ .requirement.unmet {
135+ color: #888;
136+ }
137+
138+ .requirement.unmet .requirement-icon {
139+ color: #ff4444;
140+ }
141+
142+ .error-messages {
143+ display: flex;
144+ flex-direction: column;
145+ gap: 4px;
146+ }
147+
148+ .error-message {
149+ color: #ff4444;
150+ font-size: 0.75rem;
151+ }
152+
153+ /* Compact mode styles */
154+ :global(.compact) .password-strength-indicator {
155+ font-size: 0.75rem;
156+ }
157+
158+ :global(.compact) .requirements-list {
159+ display: none;
160+ }
161+
162+ :global(.compact) .strength-bar {
163+ margin-bottom: 4px;
164+ }
165+</style>⏎̸
A src/lib/utils/security-tests.ts +373 -0
@@ -0,0 +1,373 @@
1+import { formatText, validateMessageContent, createGradientText } from './text-formatter';
2+import { validateTextStyle, DEFAULT_TEXT_STYLE } from '../types/text-formatting';
3+
4+// XSS Test Vectors
5+const XSS_TEST_VECTORS = [
6+ // Script injection
7+ '<script>alert("XSS")</script>',
8+ '<script src="malicious.js"></script>',
9+ '<img src="x" onerror="alert(\'XSS\')">',
10+ '<svg onload="alert(\'XSS\')">',
11+ '<iframe src="javascript:alert(\'XSS\')"></iframe>',
12+
13+ // Event handlers
14+ '<div onclick="alert(\'XSS\')">Click me</div>',
15+ '<input onfocus="alert(\'XSS\')" autofocus>',
16+ '<body onload="alert(\'XSS\')">',
17+ '<a href="javascript:alert(\'XSS\')">Link</a>',
18+
19+ // CSS injection
20+ '<style>body{background:url("javascript:alert(\'XSS\')")}</style>',
21+ '<div style="background-image:url(javascript:alert(\'XSS\'))">',
22+ '<div style="expression(alert(\'XSS\'))">',
23+
24+ // Data URIs
25+ '<img src="data:text/html,<script>alert(\'XSS\')</script>">',
26+ '<iframe src="data:text/html,<script>alert(\'XSS\')</script>">',
27+
28+ // HTML injection
29+ '<object data="malicious.swf">',
30+ '<embed src="malicious.swf">',
31+ '<applet code="malicious.class">',
32+
33+ // BBCode injection attempts
34+ '[script]alert("XSS")[/script]',
35+ '[img=javascript:alert("XSS")]',
36+ '[url=javascript:alert("XSS")]Link[/url]',
37+
38+ // Markdown injection attempts
39+ '![XSS](javascript:alert("XSS"))',
40+ '[XSS](javascript:alert("XSS"))',
41+
42+ // Unicode and encoding bypasses
43+ '<script>alert(&#34;XSS&#34;)</script>',
44+ '<script>alert(String.fromCharCode(88,83,83))</script>',
45+ '<img src="x" onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;">',
46+
47+ // CSS expression injection
48+ '<div style="width:expression(alert(\'XSS\'))">',
49+ '<div style="background:url(vbscript:alert(\'XSS\'))">',
50+
51+ // Mixed content attacks
52+ 'Normal text <script>alert("XSS")</script> more text',
53+ '[b]Bold <script>alert("XSS")</script> text[/b]',
54+ '**Bold <img src="x" onerror="alert(\'XSS\')"> text**'
55+];
56+
57+// CSS Injection Test Vectors
58+const CSS_INJECTION_VECTORS = [
59+ 'color: red; background: url(javascript:alert("XSS"))',
60+ 'font-family: "Arial"; } body { background: red; .fake {',
61+ 'font-size: expression(alert("XSS"))',
62+ 'background: url("data:text/html,<script>alert(\'XSS\')</script>")',
63+ 'color: red; } .injected { background: red',
64+ '@import url("malicious.css")',
65+ 'behavior: url("malicious.htc")'
66+];
67+
68+// Test Results Interface
69+interface SecurityTestResult {
70+ input: string;
71+ output: string;
72+ passed: boolean;
73+ vulnerabilities: string[];
74+ testType: string;
75+}
76+
77+interface SecurityTestSuite {
78+ xssTests: SecurityTestResult[];
79+ cssInjectionTests: SecurityTestResult[];
80+ validationTests: SecurityTestResult[];
81+ gradientTests: SecurityTestResult[];
82+ summary: {
83+ totalTests: number;
84+ passed: number;
85+ failed: number;
86+ vulnerabilities: string[];
87+ };
88+}
89+
90+// Check if output contains dangerous patterns
91+function detectVulnerabilities(output: string): string[] {
92+ const vulnerabilities: string[] = [];
93+
94+ // Script tags
95+ if (/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi.test(output)) {
96+ vulnerabilities.push('Script tag injection');
97+ }
98+
99+ // Event handlers
100+ if (/on\w+\s*=/gi.test(output)) {
101+ vulnerabilities.push('Event handler injection');
102+ }
103+
104+ // JavaScript URLs
105+ if (/javascript\s*:/gi.test(output)) {
106+ vulnerabilities.push('JavaScript URL injection');
107+ }
108+
109+ // Data URLs with HTML/JS
110+ if (/data\s*:\s*text\/html/gi.test(output)) {
111+ vulnerabilities.push('Data URL HTML injection');
112+ }
113+
114+ // CSS expressions
115+ if (/expression\s*\(/gi.test(output)) {
116+ vulnerabilities.push('CSS expression injection');
117+ }
118+
119+ // Dangerous CSS URLs
120+ if (/url\s*\(\s*['"]*javascript:/gi.test(output)) {
121+ vulnerabilities.push('CSS JavaScript URL injection');
122+ }
123+
124+ // Iframe with dangerous src
125+ if (/<iframe[^>]*src\s*=\s*['"]*(?:javascript:|data:)/gi.test(output)) {
126+ vulnerabilities.push('Dangerous iframe injection');
127+ }
128+
129+ // Object/embed tags
130+ if (/<(?:object|embed|applet)\b/gi.test(output)) {
131+ vulnerabilities.push('Dangerous object/embed tag');
132+ }
133+
134+ // Style tag injection
135+ if (/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi.test(output)) {
136+ vulnerabilities.push('Style tag injection');
137+ }
138+
139+ return vulnerabilities;
140+}
141+
142+// Run XSS tests
143+async function runXSSTests(): Promise<SecurityTestResult[]> {
144+ const results: SecurityTestResult[] = [];
145+
146+ for (const vector of XSS_TEST_VECTORS) {
147+ try {
148+ const output = await formatText(vector, DEFAULT_TEXT_STYLE, true);
149+ const vulnerabilities = detectVulnerabilities(output);
150+
151+ results.push({
152+ input: vector,
153+ output,
154+ passed: vulnerabilities.length === 0,
155+ vulnerabilities,
156+ testType: 'XSS'
157+ });
158+ } catch (error) {
159+ results.push({
160+ input: vector,
161+ output: `Error: ${error}`,
162+ passed: true, // Errors are acceptable for malicious input
163+ vulnerabilities: [],
164+ testType: 'XSS'
165+ });
166+ }
167+ }
168+
169+ return results;
170+}
171+
172+// Run CSS injection tests
173+async function runCSSInjectionTests(): Promise<SecurityTestResult[]> {
174+ const results: SecurityTestResult[] = [];
175+
176+ for (const vector of CSS_INJECTION_VECTORS) {
177+ try {
178+ const testStyle = { ...DEFAULT_TEXT_STYLE, color: 'red' as any };
179+ const output = await formatText(`Test with style`, testStyle, true);
180+ const vulnerabilities = detectVulnerabilities(output);
181+
182+ results.push({
183+ input: vector,
184+ output,
185+ passed: vulnerabilities.length === 0,
186+ vulnerabilities,
187+ testType: 'CSS Injection'
188+ });
189+ } catch (error) {
190+ results.push({
191+ input: vector,
192+ output: `Error: ${error}`,
193+ passed: true,
194+ vulnerabilities: [],
195+ testType: 'CSS Injection'
196+ });
197+ }
198+ }
199+
200+ return results;
201+}
202+
203+// Run validation tests
204+function runValidationTests(): SecurityTestResult[] {
205+ const results: SecurityTestResult[] = [];
206+
207+ const testCases = [
208+ 'Normal text',
209+ 'Text with <script>alert("XSS")</script>',
210+ 'Very long text '.repeat(200), // Test length limits
211+ '', // Empty text
212+ 'Text with\nnewlines\nand\ttabs',
213+ 'Special chars: <>&"\'`',
214+ 'Unicode: 你好世界 🌍 🎉',
215+ 'Mixed: Normal **bold** and <script>alert("XSS")</script>'
216+ ];
217+
218+ for (const testCase of testCases) {
219+ const validation = validateMessageContent(testCase);
220+ const vulnerabilities = detectVulnerabilities(validation.sanitizedContent);
221+
222+ results.push({
223+ input: testCase,
224+ output: validation.sanitizedContent,
225+ passed: validation.isValid && vulnerabilities.length === 0,
226+ vulnerabilities: [...validation.errors, ...vulnerabilities],
227+ testType: 'Validation'
228+ });
229+ }
230+
231+ return results;
232+}
233+
234+// Run gradient text security tests
235+function runGradientTests(): SecurityTestResult[] {
236+ const results: SecurityTestResult[] = [];
237+
238+ const testCases = [
239+ 'Normal gradient text',
240+ '<script>alert("XSS")</script>',
241+ 'Text with "quotes" and \'apostrophes\'',
242+ 'Very long gradient text '.repeat(50),
243+ 'Special chars: <>&\'"',
244+ ''
245+ ];
246+
247+ const testColors = ['#FF0000', '#00FF00', '#0000FF'];
248+
249+ for (const testCase of testCases) {
250+ try {
251+ const output = createGradientText(testCase, testColors);
252+ const vulnerabilities = detectVulnerabilities(output);
253+
254+ results.push({
255+ input: testCase,
256+ output,
257+ passed: vulnerabilities.length === 0,
258+ vulnerabilities,
259+ testType: 'Gradient'
260+ });
261+ } catch (error) {
262+ results.push({
263+ input: testCase,
264+ output: `Error: ${error}`,
265+ passed: true,
266+ vulnerabilities: [],
267+ testType: 'Gradient'
268+ });
269+ }
270+ }
271+
272+ return results;
273+}
274+
275+// Run complete security test suite
276+export async function runSecurityTestSuite(): Promise<SecurityTestSuite> {
277+ console.log('🔒 Running comprehensive security test suite...');
278+
279+ const xssTests = await runXSSTests();
280+ const cssInjectionTests = await runCSSInjectionTests();
281+ const validationTests = runValidationTests();
282+ const gradientTests = runGradientTests();
283+
284+ const allTests = [...xssTests, ...cssInjectionTests, ...validationTests, ...gradientTests];
285+ const passed = allTests.filter(test => test.passed).length;
286+ const failed = allTests.length - passed;
287+
288+ // Collect all unique vulnerabilities
289+ const allVulnerabilities = new Set<string>();
290+ allTests.forEach(test => {
291+ test.vulnerabilities.forEach(vuln => allVulnerabilities.add(vuln));
292+ });
293+
294+ const results: SecurityTestSuite = {
295+ xssTests,
296+ cssInjectionTests,
297+ validationTests,
298+ gradientTests,
299+ summary: {
300+ totalTests: allTests.length,
301+ passed,
302+ failed,
303+ vulnerabilities: Array.from(allVulnerabilities)
304+ }
305+ };
306+
307+ console.log(`✅ Security tests completed:`);
308+ console.log(` Total tests: ${results.summary.totalTests}`);
309+ console.log(` Passed: ${results.summary.passed}`);
310+ console.log(` Failed: ${results.summary.failed}`);
311+
312+ if (results.summary.vulnerabilities.length > 0) {
313+ console.warn(`⚠️ Vulnerabilities detected:`, results.summary.vulnerabilities);
314+ } else {
315+ console.log(`🛡️ No vulnerabilities detected!`);
316+ }
317+
318+ return results;
319+}
320+
321+// Quick security check function
322+export async function quickSecurityCheck(input: string): Promise<boolean> {
323+ try {
324+ const output = await formatText(input, DEFAULT_TEXT_STYLE, true);
325+ const vulnerabilities = detectVulnerabilities(output);
326+ return vulnerabilities.length === 0;
327+ } catch (error) {
328+ // Errors on malicious input are acceptable
329+ return true;
330+ }
331+}
332+
333+// Style validation tests
334+export function testStyleValidation(): boolean {
335+ const testCases = [
336+ // Valid styles
337+ { fontFamily: 'tahoma', fontSize: 14, color: 'red', bold: true },
338+ { fontFamily: 'verdana', fontSize: 16, color: 'blue', italic: true },
339+
340+ // Invalid styles (should be sanitized)
341+ { fontFamily: 'malicious-font', fontSize: 999, color: 'invalid-color' },
342+ { fontFamily: 'tahoma', fontSize: -10, color: 'red' },
343+ { fontFamily: 'tahoma', fontSize: 'invalid' as any, color: 'red' }
344+ ];
345+
346+ for (const testCase of testCases) {
347+ try {
348+ const validated = validateTextStyle(testCase as Partial<TextStyle>);
349+
350+ // Check that validated style has safe values
351+ if (!validated.fontFamily ||
352+ validated.fontSize < 8 ||
353+ validated.fontSize > 72) {
354+ console.error('Style validation failed for:', testCase);
355+ return false;
356+ }
357+ } catch (error) {
358+ console.error('Style validation error:', error);
359+ return false;
360+ }
361+ }
362+
363+ return true;
364+}
365+
366+// Export test utilities
367+export {
368+ XSS_TEST_VECTORS,
369+ CSS_INJECTION_VECTORS,
370+ detectVulnerabilities,
371+ type SecurityTestResult,
372+ type SecurityTestSuite
373+};⏎̸
@@ -0,0 +1,373 @@
1+import { formatText, validateMessageContent, createGradientText } from './text-formatter';
2+import { validateTextStyle, DEFAULT_TEXT_STYLE } from '../types/text-formatting';
3+
4+// XSS Test Vectors
5+const XSS_TEST_VECTORS = [
6+ // Script injection
7+ '<script>alert("XSS")</script>',
8+ '<script src="malicious.js"></script>',
9+ '<img src="x" onerror="alert(\'XSS\')">',
10+ '<svg onload="alert(\'XSS\')">',
11+ '<iframe src="javascript:alert(\'XSS\')"></iframe>',
12+
13+ // Event handlers
14+ '<div onclick="alert(\'XSS\')">Click me</div>',
15+ '<input onfocus="alert(\'XSS\')" autofocus>',
16+ '<body onload="alert(\'XSS\')">',
17+ '<a href="javascript:alert(\'XSS\')">Link</a>',
18+
19+ // CSS injection
20+ '<style>body{background:url("javascript:alert(\'XSS\')")}</style>',
21+ '<div style="background-image:url(javascript:alert(\'XSS\'))">',
22+ '<div style="expression(alert(\'XSS\'))">',
23+
24+ // Data URIs
25+ '<img src="data:text/html,<script>alert(\'XSS\')</script>">',
26+ '<iframe src="data:text/html,<script>alert(\'XSS\')</script>">',
27+
28+ // HTML injection
29+ '<object data="malicious.swf">',
30+ '<embed src="malicious.swf">',
31+ '<applet code="malicious.class">',
32+
33+ // BBCode injection attempts
34+ '[script]alert("XSS")[/script]',
35+ '[img=javascript:alert("XSS")]',
36+ '[url=javascript:alert("XSS")]Link[/url]',
37+
38+ // Markdown injection attempts
39+ '![XSS](javascript:alert("XSS"))',
40+ '[XSS](javascript:alert("XSS"))',
41+
42+ // Unicode and encoding bypasses
43+ '<script>alert(&#34;XSS&#34;)</script>',
44+ '<script>alert(String.fromCharCode(88,83,83))</script>',
45+ '<img src="x" onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;">',
46+
47+ // CSS expression injection
48+ '<div style="width:expression(alert(\'XSS\'))">',
49+ '<div style="background:url(vbscript:alert(\'XSS\'))">',
50+
51+ // Mixed content attacks
52+ 'Normal text <script>alert("XSS")</script> more text',
53+ '[b]Bold <script>alert("XSS")</script> text[/b]',
54+ '**Bold <img src="x" onerror="alert(\'XSS\')"> text**'
55+];
56+
57+// CSS Injection Test Vectors
58+const CSS_INJECTION_VECTORS = [
59+ 'color: red; background: url(javascript:alert("XSS"))',
60+ 'font-family: "Arial"; } body { background: red; .fake {',
61+ 'font-size: expression(alert("XSS"))',
62+ 'background: url("data:text/html,<script>alert(\'XSS\')</script>")',
63+ 'color: red; } .injected { background: red',
64+ '@import url("malicious.css")',
65+ 'behavior: url("malicious.htc")'
66+];
67+
68+// Test Results Interface
69+interface SecurityTestResult {
70+ input: string;
71+ output: string;
72+ passed: boolean;
73+ vulnerabilities: string[];
74+ testType: string;
75+}
76+
77+interface SecurityTestSuite {
78+ xssTests: SecurityTestResult[];
79+ cssInjectionTests: SecurityTestResult[];
80+ validationTests: SecurityTestResult[];
81+ gradientTests: SecurityTestResult[];
82+ summary: {
83+ totalTests: number;
84+ passed: number;
85+ failed: number;
86+ vulnerabilities: string[];
87+ };
88+}
89+
90+// Check if output contains dangerous patterns
91+function detectVulnerabilities(output: string): string[] {
92+ const vulnerabilities: string[] = [];
93+
94+ // Script tags
95+ if (/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi.test(output)) {
96+ vulnerabilities.push('Script tag injection');
97+ }
98+
99+ // Event handlers
100+ if (/on\w+\s*=/gi.test(output)) {
101+ vulnerabilities.push('Event handler injection');
102+ }
103+
104+ // JavaScript URLs
105+ if (/javascript\s*:/gi.test(output)) {
106+ vulnerabilities.push('JavaScript URL injection');
107+ }
108+
109+ // Data URLs with HTML/JS
110+ if (/data\s*:\s*text\/html/gi.test(output)) {
111+ vulnerabilities.push('Data URL HTML injection');
112+ }
113+
114+ // CSS expressions
115+ if (/expression\s*\(/gi.test(output)) {
116+ vulnerabilities.push('CSS expression injection');
117+ }
118+
119+ // Dangerous CSS URLs
120+ if (/url\s*\(\s*['"]*javascript:/gi.test(output)) {
121+ vulnerabilities.push('CSS JavaScript URL injection');
122+ }
123+
124+ // Iframe with dangerous src
125+ if (/<iframe[^>]*src\s*=\s*['"]*(?:javascript:|data:)/gi.test(output)) {
126+ vulnerabilities.push('Dangerous iframe injection');
127+ }
128+
129+ // Object/embed tags
130+ if (/<(?:object|embed|applet)\b/gi.test(output)) {
131+ vulnerabilities.push('Dangerous object/embed tag');
132+ }
133+
134+ // Style tag injection
135+ if (/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi.test(output)) {
136+ vulnerabilities.push('Style tag injection');
137+ }
138+
139+ return vulnerabilities;
140+}
141+
142+// Run XSS tests
143+async function runXSSTests(): Promise<SecurityTestResult[]> {
144+ const results: SecurityTestResult[] = [];
145+
146+ for (const vector of XSS_TEST_VECTORS) {
147+ try {
148+ const output = await formatText(vector, DEFAULT_TEXT_STYLE, true);
149+ const vulnerabilities = detectVulnerabilities(output);
150+
151+ results.push({
152+ input: vector,
153+ output,
154+ passed: vulnerabilities.length === 0,
155+ vulnerabilities,
156+ testType: 'XSS'
157+ });
158+ } catch (error) {
159+ results.push({
160+ input: vector,
161+ output: `Error: ${error}`,
162+ passed: true, // Errors are acceptable for malicious input
163+ vulnerabilities: [],
164+ testType: 'XSS'
165+ });
166+ }
167+ }
168+
169+ return results;
170+}
171+
172+// Run CSS injection tests
173+async function runCSSInjectionTests(): Promise<SecurityTestResult[]> {
174+ const results: SecurityTestResult[] = [];
175+
176+ for (const vector of CSS_INJECTION_VECTORS) {
177+ try {
178+ const testStyle = { ...DEFAULT_TEXT_STYLE, color: 'red' as any };
179+ const output = await formatText(`Test with style`, testStyle, true);
180+ const vulnerabilities = detectVulnerabilities(output);
181+
182+ results.push({
183+ input: vector,
184+ output,
185+ passed: vulnerabilities.length === 0,
186+ vulnerabilities,
187+ testType: 'CSS Injection'
188+ });
189+ } catch (error) {
190+ results.push({
191+ input: vector,
192+ output: `Error: ${error}`,
193+ passed: true,
194+ vulnerabilities: [],
195+ testType: 'CSS Injection'
196+ });
197+ }
198+ }
199+
200+ return results;
201+}
202+
203+// Run validation tests
204+function runValidationTests(): SecurityTestResult[] {
205+ const results: SecurityTestResult[] = [];
206+
207+ const testCases = [
208+ 'Normal text',
209+ 'Text with <script>alert("XSS")</script>',
210+ 'Very long text '.repeat(200), // Test length limits
211+ '', // Empty text
212+ 'Text with\nnewlines\nand\ttabs',
213+ 'Special chars: <>&"\'`',
214+ 'Unicode: 你好世界 🌍 🎉',
215+ 'Mixed: Normal **bold** and <script>alert("XSS")</script>'
216+ ];
217+
218+ for (const testCase of testCases) {
219+ const validation = validateMessageContent(testCase);
220+ const vulnerabilities = detectVulnerabilities(validation.sanitizedContent);
221+
222+ results.push({
223+ input: testCase,
224+ output: validation.sanitizedContent,
225+ passed: validation.isValid && vulnerabilities.length === 0,
226+ vulnerabilities: [...validation.errors, ...vulnerabilities],
227+ testType: 'Validation'
228+ });
229+ }
230+
231+ return results;
232+}
233+
234+// Run gradient text security tests
235+function runGradientTests(): SecurityTestResult[] {
236+ const results: SecurityTestResult[] = [];
237+
238+ const testCases = [
239+ 'Normal gradient text',
240+ '<script>alert("XSS")</script>',
241+ 'Text with "quotes" and \'apostrophes\'',
242+ 'Very long gradient text '.repeat(50),
243+ 'Special chars: <>&\'"',
244+ ''
245+ ];
246+
247+ const testColors = ['#FF0000', '#00FF00', '#0000FF'];
248+
249+ for (const testCase of testCases) {
250+ try {
251+ const output = createGradientText(testCase, testColors);
252+ const vulnerabilities = detectVulnerabilities(output);
253+
254+ results.push({
255+ input: testCase,
256+ output,
257+ passed: vulnerabilities.length === 0,
258+ vulnerabilities,
259+ testType: 'Gradient'
260+ });
261+ } catch (error) {
262+ results.push({
263+ input: testCase,
264+ output: `Error: ${error}`,
265+ passed: true,
266+ vulnerabilities: [],
267+ testType: 'Gradient'
268+ });
269+ }
270+ }
271+
272+ return results;
273+}
274+
275+// Run complete security test suite
276+export async function runSecurityTestSuite(): Promise<SecurityTestSuite> {
277+ console.log('🔒 Running comprehensive security test suite...');
278+
279+ const xssTests = await runXSSTests();
280+ const cssInjectionTests = await runCSSInjectionTests();
281+ const validationTests = runValidationTests();
282+ const gradientTests = runGradientTests();
283+
284+ const allTests = [...xssTests, ...cssInjectionTests, ...validationTests, ...gradientTests];
285+ const passed = allTests.filter(test => test.passed).length;
286+ const failed = allTests.length - passed;
287+
288+ // Collect all unique vulnerabilities
289+ const allVulnerabilities = new Set<string>();
290+ allTests.forEach(test => {
291+ test.vulnerabilities.forEach(vuln => allVulnerabilities.add(vuln));
292+ });
293+
294+ const results: SecurityTestSuite = {
295+ xssTests,
296+ cssInjectionTests,
297+ validationTests,
298+ gradientTests,
299+ summary: {
300+ totalTests: allTests.length,
301+ passed,
302+ failed,
303+ vulnerabilities: Array.from(allVulnerabilities)
304+ }
305+ };
306+
307+ console.log(`✅ Security tests completed:`);
308+ console.log(` Total tests: ${results.summary.totalTests}`);
309+ console.log(` Passed: ${results.summary.passed}`);
310+ console.log(` Failed: ${results.summary.failed}`);
311+
312+ if (results.summary.vulnerabilities.length > 0) {
313+ console.warn(`⚠️ Vulnerabilities detected:`, results.summary.vulnerabilities);
314+ } else {
315+ console.log(`🛡️ No vulnerabilities detected!`);
316+ }
317+
318+ return results;
319+}
320+
321+// Quick security check function
322+export async function quickSecurityCheck(input: string): Promise<boolean> {
323+ try {
324+ const output = await formatText(input, DEFAULT_TEXT_STYLE, true);
325+ const vulnerabilities = detectVulnerabilities(output);
326+ return vulnerabilities.length === 0;
327+ } catch (error) {
328+ // Errors on malicious input are acceptable
329+ return true;
330+ }
331+}
332+
333+// Style validation tests
334+export function testStyleValidation(): boolean {
335+ const testCases = [
336+ // Valid styles
337+ { fontFamily: 'tahoma', fontSize: 14, color: 'red', bold: true },
338+ { fontFamily: 'verdana', fontSize: 16, color: 'blue', italic: true },
339+
340+ // Invalid styles (should be sanitized)
341+ { fontFamily: 'malicious-font', fontSize: 999, color: 'invalid-color' },
342+ { fontFamily: 'tahoma', fontSize: -10, color: 'red' },
343+ { fontFamily: 'tahoma', fontSize: 'invalid' as any, color: 'red' }
344+ ];
345+
346+ for (const testCase of testCases) {
347+ try {
348+ const validated = validateTextStyle(testCase as Partial<TextStyle>);
349+
350+ // Check that validated style has safe values
351+ if (!validated.fontFamily ||
352+ validated.fontSize < 8 ||
353+ validated.fontSize > 72) {
354+ console.error('Style validation failed for:', testCase);
355+ return false;
356+ }
357+ } catch (error) {
358+ console.error('Style validation error:', error);
359+ return false;
360+ }
361+ }
362+
363+ return true;
364+}
365+
366+// Export test utilities
367+export {
368+ XSS_TEST_VECTORS,
369+ CSS_INJECTION_VECTORS,
370+ detectVulnerabilities,
371+ type SecurityTestResult,
372+ type SecurityTestSuite
373+};⏎̸
A src/lib/validation/password.ts +199 -0
@@ -0,0 +1,199 @@
1+import { z } from 'zod';
2+
3+// Dynamic password constraints configuration
4+export interface PasswordConstraints {
5+ minLength: number;
6+ maxLength: number;
7+ requireUppercase: boolean;
8+ requireLowercase: boolean;
9+ requireNumbers: boolean;
10+ requireSpecialChars: boolean;
11+ forbiddenPatterns: string[];
12+}
13+
14+// Default password constraints
15+export const DEFAULT_PASSWORD_CONSTRAINTS: PasswordConstraints = {
16+ minLength: 8,
17+ maxLength: 64,
18+ requireUppercase: true,
19+ requireLowercase: true,
20+ requireNumbers: true,
21+ requireSpecialChars: false,
22+ forbiddenPatterns: ['password', '123456', 'qwerty', 'admin']
23+};
24+
25+// Validation error messages
26+export const PASSWORD_ERROR_MESSAGES = {
27+ minLength: (min: number) => `Password must be at least ${min} characters long`,
28+ maxLength: (max: number) => `Password must be at most ${max} characters long`,
29+ requireUppercase: 'Password must contain at least one uppercase letter',
30+ requireLowercase: 'Password must contain at least one lowercase letter',
31+ requireNumbers: 'Password must contain at least one number',
32+ requireSpecialChars: 'Password must contain at least one special character (!@#$%^&*)',
33+ forbiddenPattern: (pattern: string) => `Password cannot contain '${pattern}'`,
34+ common: 'Password is too common or weak'
35+};
36+
37+// Create dynamic password schema based on constraints
38+export function createPasswordSchema(constraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS) {
39+ let schema = z.string()
40+ .min(constraints.minLength, PASSWORD_ERROR_MESSAGES.minLength(constraints.minLength))
41+ .max(constraints.maxLength, PASSWORD_ERROR_MESSAGES.maxLength(constraints.maxLength));
42+
43+ // Add character type requirements
44+ if (constraints.requireUppercase) {
45+ schema = schema.regex(/[A-Z]/, PASSWORD_ERROR_MESSAGES.requireUppercase);
46+ }
47+
48+ if (constraints.requireLowercase) {
49+ schema = schema.regex(/[a-z]/, PASSWORD_ERROR_MESSAGES.requireLowercase);
50+ }
51+
52+ if (constraints.requireNumbers) {
53+ schema = schema.regex(/\d/, PASSWORD_ERROR_MESSAGES.requireNumbers);
54+ }
55+
56+ if (constraints.requireSpecialChars) {
57+ schema = schema.regex(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/, PASSWORD_ERROR_MESSAGES.requireSpecialChars);
58+ }
59+
60+ // Add forbidden patterns check
61+ if (constraints.forbiddenPatterns.length > 0) {
62+ return schema.refine(
63+ (password) => {
64+ const lowerPassword = password.toLowerCase();
65+ return !constraints.forbiddenPatterns.some(pattern =>
66+ lowerPassword.includes(pattern.toLowerCase())
67+ );
68+ },
69+ {
70+ message: PASSWORD_ERROR_MESSAGES.common
71+ }
72+ );
73+ }
74+
75+ return schema;
76+}
77+
78+// Login validation schema
79+export const loginSchema = z.object({
80+ username: z.string()
81+ .min(3, 'Username must be at least 3 characters')
82+ .max(32, 'Username must be at most 32 characters')
83+ .regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and dashes'),
84+ password: z.string().min(1, 'Password is required')
85+});
86+
87+// Registration validation schema (using dynamic password validation)
88+export function createRegistrationSchema(constraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS) {
89+ return z.object({
90+ suUsername: z.string()
91+ .min(3, 'Username must be at least 3 characters')
92+ .max(32, 'Username must be at most 32 characters')
93+ .regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and dashes'),
94+ suPassword: createPasswordSchema(constraints),
95+ suConfirmPassword: z.string(),
96+ captchaAnswer: z.string().min(1, 'Please answer the PDR question'),
97+ turnstileToken: z.string().optional()
98+ }).refine(
99+ (data) => data.suPassword === data.suConfirmPassword,
100+ {
101+ message: 'Passwords do not match',
102+ path: ['suConfirmPassword']
103+ }
104+ );
105+}
106+
107+// Utility function to validate password strength and return detailed feedback
108+export function validatePasswordStrength(password: string, constraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS) {
109+ const errors: string[] = [];
110+ const requirements: { met: boolean; message: string }[] = [];
111+
112+ // Length checks
113+ if (password.length < constraints.minLength) {
114+ errors.push(PASSWORD_ERROR_MESSAGES.minLength(constraints.minLength));
115+ }
116+ requirements.push({
117+ met: password.length >= constraints.minLength,
118+ message: `At least ${constraints.minLength} characters`
119+ });
120+
121+ if (password.length > constraints.maxLength) {
122+ errors.push(PASSWORD_ERROR_MESSAGES.maxLength(constraints.maxLength));
123+ }
124+
125+ // Character type checks
126+ if (constraints.requireUppercase) {
127+ const hasUppercase = /[A-Z]/.test(password);
128+ if (!hasUppercase) errors.push(PASSWORD_ERROR_MESSAGES.requireUppercase);
129+ requirements.push({
130+ met: hasUppercase,
131+ message: 'At least one uppercase letter'
132+ });
133+ }
134+
135+ if (constraints.requireLowercase) {
136+ const hasLowercase = /[a-z]/.test(password);
137+ if (!hasLowercase) errors.push(PASSWORD_ERROR_MESSAGES.requireLowercase);
138+ requirements.push({
139+ met: hasLowercase,
140+ message: 'At least one lowercase letter'
141+ });
142+ }
143+
144+ if (constraints.requireNumbers) {
145+ const hasNumbers = /\d/.test(password);
146+ if (!hasNumbers) errors.push(PASSWORD_ERROR_MESSAGES.requireNumbers);
147+ requirements.push({
148+ met: hasNumbers,
149+ message: 'At least one number'
150+ });
151+ }
152+
153+ if (constraints.requireSpecialChars) {
154+ const hasSpecialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
155+ if (!hasSpecialChars) errors.push(PASSWORD_ERROR_MESSAGES.requireSpecialChars);
156+ requirements.push({
157+ met: hasSpecialChars,
158+ message: 'At least one special character'
159+ });
160+ }
161+
162+ // Forbidden patterns check
163+ if (constraints.forbiddenPatterns.length > 0) {
164+ const lowerPassword = password.toLowerCase();
165+ const hasForbiddenPattern = constraints.forbiddenPatterns.some(pattern =>
166+ lowerPassword.includes(pattern.toLowerCase())
167+ );
168+ if (hasForbiddenPattern) {
169+ errors.push(PASSWORD_ERROR_MESSAGES.common);
170+ }
171+ requirements.push({
172+ met: !hasForbiddenPattern,
173+ message: 'Not a common or weak password'
174+ });
175+ }
176+
177+ return {
178+ isValid: errors.length === 0,
179+ errors,
180+ requirements,
181+ strength: calculatePasswordStrength(password, requirements)
182+ };
183+}
184+
185+// Calculate password strength score (0-100)
186+function calculatePasswordStrength(password: string, requirements: { met: boolean; message: string }[]): number {
187+ const metRequirements = requirements.filter(req => req.met).length;
188+ const totalRequirements = requirements.length;
189+
190+ if (totalRequirements === 0) return 0;
191+
192+ let baseScore = (metRequirements / totalRequirements) * 80;
193+
194+ // Bonus points for length beyond minimum
195+ if (password.length > 12) baseScore += 10;
196+ if (password.length > 16) baseScore += 10;
197+
198+ return Math.min(100, Math.round(baseScore));
199+}⏎̸
@@ -0,0 +1,199 @@
1+import { z } from 'zod';
2+
3+// Dynamic password constraints configuration
4+export interface PasswordConstraints {
5+ minLength: number;
6+ maxLength: number;
7+ requireUppercase: boolean;
8+ requireLowercase: boolean;
9+ requireNumbers: boolean;
10+ requireSpecialChars: boolean;
11+ forbiddenPatterns: string[];
12+}
13+
14+// Default password constraints
15+export const DEFAULT_PASSWORD_CONSTRAINTS: PasswordConstraints = {
16+ minLength: 8,
17+ maxLength: 64,
18+ requireUppercase: true,
19+ requireLowercase: true,
20+ requireNumbers: true,
21+ requireSpecialChars: false,
22+ forbiddenPatterns: ['password', '123456', 'qwerty', 'admin']
23+};
24+
25+// Validation error messages
26+export const PASSWORD_ERROR_MESSAGES = {
27+ minLength: (min: number) => `Password must be at least ${min} characters long`,
28+ maxLength: (max: number) => `Password must be at most ${max} characters long`,
29+ requireUppercase: 'Password must contain at least one uppercase letter',
30+ requireLowercase: 'Password must contain at least one lowercase letter',
31+ requireNumbers: 'Password must contain at least one number',
32+ requireSpecialChars: 'Password must contain at least one special character (!@#$%^&*)',
33+ forbiddenPattern: (pattern: string) => `Password cannot contain '${pattern}'`,
34+ common: 'Password is too common or weak'
35+};
36+
37+// Create dynamic password schema based on constraints
38+export function createPasswordSchema(constraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS) {
39+ let schema = z.string()
40+ .min(constraints.minLength, PASSWORD_ERROR_MESSAGES.minLength(constraints.minLength))
41+ .max(constraints.maxLength, PASSWORD_ERROR_MESSAGES.maxLength(constraints.maxLength));
42+
43+ // Add character type requirements
44+ if (constraints.requireUppercase) {
45+ schema = schema.regex(/[A-Z]/, PASSWORD_ERROR_MESSAGES.requireUppercase);
46+ }
47+
48+ if (constraints.requireLowercase) {
49+ schema = schema.regex(/[a-z]/, PASSWORD_ERROR_MESSAGES.requireLowercase);
50+ }
51+
52+ if (constraints.requireNumbers) {
53+ schema = schema.regex(/\d/, PASSWORD_ERROR_MESSAGES.requireNumbers);
54+ }
55+
56+ if (constraints.requireSpecialChars) {
57+ schema = schema.regex(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/, PASSWORD_ERROR_MESSAGES.requireSpecialChars);
58+ }
59+
60+ // Add forbidden patterns check
61+ if (constraints.forbiddenPatterns.length > 0) {
62+ return schema.refine(
63+ (password) => {
64+ const lowerPassword = password.toLowerCase();
65+ return !constraints.forbiddenPatterns.some(pattern =>
66+ lowerPassword.includes(pattern.toLowerCase())
67+ );
68+ },
69+ {
70+ message: PASSWORD_ERROR_MESSAGES.common
71+ }
72+ );
73+ }
74+
75+ return schema;
76+}
77+
78+// Login validation schema
79+export const loginSchema = z.object({
80+ username: z.string()
81+ .min(3, 'Username must be at least 3 characters')
82+ .max(32, 'Username must be at most 32 characters')
83+ .regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and dashes'),
84+ password: z.string().min(1, 'Password is required')
85+});
86+
87+// Registration validation schema (using dynamic password validation)
88+export function createRegistrationSchema(constraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS) {
89+ return z.object({
90+ suUsername: z.string()
91+ .min(3, 'Username must be at least 3 characters')
92+ .max(32, 'Username must be at most 32 characters')
93+ .regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, underscores, and dashes'),
94+ suPassword: createPasswordSchema(constraints),
95+ suConfirmPassword: z.string(),
96+ captchaAnswer: z.string().min(1, 'Please answer the PDR question'),
97+ turnstileToken: z.string().optional()
98+ }).refine(
99+ (data) => data.suPassword === data.suConfirmPassword,
100+ {
101+ message: 'Passwords do not match',
102+ path: ['suConfirmPassword']
103+ }
104+ );
105+}
106+
107+// Utility function to validate password strength and return detailed feedback
108+export function validatePasswordStrength(password: string, constraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS) {
109+ const errors: string[] = [];
110+ const requirements: { met: boolean; message: string }[] = [];
111+
112+ // Length checks
113+ if (password.length < constraints.minLength) {
114+ errors.push(PASSWORD_ERROR_MESSAGES.minLength(constraints.minLength));
115+ }
116+ requirements.push({
117+ met: password.length >= constraints.minLength,
118+ message: `At least ${constraints.minLength} characters`
119+ });
120+
121+ if (password.length > constraints.maxLength) {
122+ errors.push(PASSWORD_ERROR_MESSAGES.maxLength(constraints.maxLength));
123+ }
124+
125+ // Character type checks
126+ if (constraints.requireUppercase) {
127+ const hasUppercase = /[A-Z]/.test(password);
128+ if (!hasUppercase) errors.push(PASSWORD_ERROR_MESSAGES.requireUppercase);
129+ requirements.push({
130+ met: hasUppercase,
131+ message: 'At least one uppercase letter'
132+ });
133+ }
134+
135+ if (constraints.requireLowercase) {
136+ const hasLowercase = /[a-z]/.test(password);
137+ if (!hasLowercase) errors.push(PASSWORD_ERROR_MESSAGES.requireLowercase);
138+ requirements.push({
139+ met: hasLowercase,
140+ message: 'At least one lowercase letter'
141+ });
142+ }
143+
144+ if (constraints.requireNumbers) {
145+ const hasNumbers = /\d/.test(password);
146+ if (!hasNumbers) errors.push(PASSWORD_ERROR_MESSAGES.requireNumbers);
147+ requirements.push({
148+ met: hasNumbers,
149+ message: 'At least one number'
150+ });
151+ }
152+
153+ if (constraints.requireSpecialChars) {
154+ const hasSpecialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
155+ if (!hasSpecialChars) errors.push(PASSWORD_ERROR_MESSAGES.requireSpecialChars);
156+ requirements.push({
157+ met: hasSpecialChars,
158+ message: 'At least one special character'
159+ });
160+ }
161+
162+ // Forbidden patterns check
163+ if (constraints.forbiddenPatterns.length > 0) {
164+ const lowerPassword = password.toLowerCase();
165+ const hasForbiddenPattern = constraints.forbiddenPatterns.some(pattern =>
166+ lowerPassword.includes(pattern.toLowerCase())
167+ );
168+ if (hasForbiddenPattern) {
169+ errors.push(PASSWORD_ERROR_MESSAGES.common);
170+ }
171+ requirements.push({
172+ met: !hasForbiddenPattern,
173+ message: 'Not a common or weak password'
174+ });
175+ }
176+
177+ return {
178+ isValid: errors.length === 0,
179+ errors,
180+ requirements,
181+ strength: calculatePasswordStrength(password, requirements)
182+ };
183+}
184+
185+// Calculate password strength score (0-100)
186+function calculatePasswordStrength(password: string, requirements: { met: boolean; message: string }[]): number {
187+ const metRequirements = requirements.filter(req => req.met).length;
188+ const totalRequirements = requirements.length;
189+
190+ if (totalRequirements === 0) return 0;
191+
192+ let baseScore = (metRequirements / totalRequirements) * 80;
193+
194+ // Bonus points for length beyond minimum
195+ if (password.length > 12) baseScore += 10;
196+ if (password.length > 16) baseScore += 10;
197+
198+ return Math.min(100, Math.round(baseScore));
199+}⏎̸
A src/lib/validation/text-formatting.ts +81 -0
@@ -0,0 +1,81 @@
1+import type { TextStyle } from '$lib/types/text-formatting';
2+import { RETRO_FONTS } from '$lib/types/text-formatting';
3+
4+const MIN_FONT_SIZE = 8;
5+const MAX_FONT_SIZE = 18;
6+const MAX_GRADIENT_COLORS = 3;
7+const HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/;
8+
9+export function validateHexColor(color: string): boolean {
10+ return HEX_COLOR_REGEX.test(color);
11+}
12+
13+export function validateFontFamily(fontFamily: string): boolean {
14+ return fontFamily in RETRO_FONTS;
15+}
16+
17+export function validateFontSize(size: number): number {
18+ return Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size));
19+}
20+
21+export function validateGradient(gradient: unknown): string[] | undefined {
22+ if (!Array.isArray(gradient)) return undefined;
23+ if (gradient.length === 0 || gradient.length > MAX_GRADIENT_COLORS) return undefined;
24+
25+ const validColors = gradient.filter(color =>
26+ typeof color === 'string' && validateHexColor(color)
27+ );
28+
29+ return validColors.length === gradient.length ? validColors : undefined;
30+}
31+
32+export function validateTextStyle(style: unknown): TextStyle | null {
33+ if (!style || typeof style !== 'object') return null;
34+
35+ const rawStyle = style as Record<string, unknown>;
36+
37+ const validated: TextStyle = {
38+ fontFamily: 'pixelated',
39+ fontSize: 14,
40+ bold: false,
41+ italic: false,
42+ underline: false,
43+ strikethrough: false
44+ };
45+
46+ // Validate font family
47+ if (typeof rawStyle.fontFamily === 'string' && validateFontFamily(rawStyle.fontFamily)) {
48+ validated.fontFamily = rawStyle.fontFamily as keyof typeof RETRO_FONTS;
49+ }
50+
51+ // Validate font size
52+ if (typeof rawStyle.fontSize === 'number') {
53+ validated.fontSize = validateFontSize(rawStyle.fontSize);
54+ }
55+
56+ // Validate color
57+ if (typeof rawStyle.color === 'string' && validateHexColor(rawStyle.color)) {
58+ validated.color = rawStyle.color;
59+ }
60+
61+ // Validate gradient
62+ const validatedGradient = validateGradient(rawStyle.gradient);
63+ if (validatedGradient) {
64+ validated.gradient = validatedGradient;
65+ }
66+
67+ // Validate boolean properties
68+ validated.bold = Boolean(rawStyle.bold);
69+ validated.italic = Boolean(rawStyle.italic);
70+ validated.underline = Boolean(rawStyle.underline);
71+ validated.strikethrough = Boolean(rawStyle.strikethrough);
72+
73+ return validated;
74+}
75+
76+export function sanitizeStyleData(styleData: unknown): TextStyle | undefined {
77+ if (!styleData) return undefined;
78+
79+ const validated = validateTextStyle(styleData);
80+ return validated || undefined;
81+}⏎̸
@@ -0,0 +1,81 @@
1+import type { TextStyle } from '$lib/types/text-formatting';
2+import { RETRO_FONTS } from '$lib/types/text-formatting';
3+
4+const MIN_FONT_SIZE = 8;
5+const MAX_FONT_SIZE = 18;
6+const MAX_GRADIENT_COLORS = 3;
7+const HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/;
8+
9+export function validateHexColor(color: string): boolean {
10+ return HEX_COLOR_REGEX.test(color);
11+}
12+
13+export function validateFontFamily(fontFamily: string): boolean {
14+ return fontFamily in RETRO_FONTS;
15+}
16+
17+export function validateFontSize(size: number): number {
18+ return Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size));
19+}
20+
21+export function validateGradient(gradient: unknown): string[] | undefined {
22+ if (!Array.isArray(gradient)) return undefined;
23+ if (gradient.length === 0 || gradient.length > MAX_GRADIENT_COLORS) return undefined;
24+
25+ const validColors = gradient.filter(color =>
26+ typeof color === 'string' && validateHexColor(color)
27+ );
28+
29+ return validColors.length === gradient.length ? validColors : undefined;
30+}
31+
32+export function validateTextStyle(style: unknown): TextStyle | null {
33+ if (!style || typeof style !== 'object') return null;
34+
35+ const rawStyle = style as Record<string, unknown>;
36+
37+ const validated: TextStyle = {
38+ fontFamily: 'pixelated',
39+ fontSize: 14,
40+ bold: false,
41+ italic: false,
42+ underline: false,
43+ strikethrough: false
44+ };
45+
46+ // Validate font family
47+ if (typeof rawStyle.fontFamily === 'string' && validateFontFamily(rawStyle.fontFamily)) {
48+ validated.fontFamily = rawStyle.fontFamily as keyof typeof RETRO_FONTS;
49+ }
50+
51+ // Validate font size
52+ if (typeof rawStyle.fontSize === 'number') {
53+ validated.fontSize = validateFontSize(rawStyle.fontSize);
54+ }
55+
56+ // Validate color
57+ if (typeof rawStyle.color === 'string' && validateHexColor(rawStyle.color)) {
58+ validated.color = rawStyle.color;
59+ }
60+
61+ // Validate gradient
62+ const validatedGradient = validateGradient(rawStyle.gradient);
63+ if (validatedGradient) {
64+ validated.gradient = validatedGradient;
65+ }
66+
67+ // Validate boolean properties
68+ validated.bold = Boolean(rawStyle.bold);
69+ validated.italic = Boolean(rawStyle.italic);
70+ validated.underline = Boolean(rawStyle.underline);
71+ validated.strikethrough = Boolean(rawStyle.strikethrough);
72+
73+ return validated;
74+}
75+
76+export function sanitizeStyleData(styleData: unknown): TextStyle | undefined {
77+ if (!styleData) return undefined;
78+
79+ const validated = validateTextStyle(styleData);
80+ return validated || undefined;
81+}⏎̸

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close