Plugin & Theme System: - Add plugin type definitions, permissions (30+), and validation constants - Add IndexedDB storage layer for plugin code, theme CSS, and previews - Add theme CSS sanitization, injection, and safety validation - Add HookBus event system with 130+ hooks across 20 domains - Add plugin ZIP extraction and manifest validation with JS security checks - Add sandboxed PluginAPI factory with scoped storage, logging, and permission gating - Add plugin loader with blob URL dynamic import and auto-disable circuit breaker - Add 3 built-in themes (Nord, Catppuccin, Solarized) - Add Zustand plugin store with install/uninstall/enable/disable lifecycle - Add PluginSlot, PluginSlotRenderer, and PluginErrorBoundary components - Add plugins and themes settings UI panels - Integrate plugin slots into email viewer, composer, navigation rail, sidebar, and context menu - Extend theme store with custom theme installation and activation Admin Dashboard: - Add admin authentication with scrypt password hashing and AES-256-GCM sessions - Add rate-limited login (5 attempts/15min per IP) - Add config manager with admin override > env var > default priority - Add settings policy system with feature gates and per-setting restrictions - Add audit logging with rotation - Add admin API routes (login, logout, config, policy, audit, password change) - Add admin UI pages (login, dashboard, config, policy, audit) - Add policy store for client-side feature gate enforcement - Wire admin password initialization into server instrumentation Tests: - Add 139 tests across 10 test files covering all plugin/theme modules
116 lines
3.1 KiB
TypeScript
116 lines
3.1 KiB
TypeScript
// Theme CSS injection and sanitization
|
|
|
|
import { DISALLOWED_CSS_PATTERNS } from './plugin-types';
|
|
|
|
const THEME_STYLE_ID = 'active-theme';
|
|
|
|
/**
|
|
* Sanitize theme CSS: strip dangerous patterns like @import, external url(),
|
|
* JavaScript expressions, and -moz-binding. Returns cleaned CSS.
|
|
*/
|
|
export function sanitizeThemeCSS(css: string): { css: string; warnings: string[] } {
|
|
const warnings: string[] = [];
|
|
let cleaned = css;
|
|
|
|
for (const pattern of DISALLOWED_CSS_PATTERNS) {
|
|
if (pattern.test(cleaned)) {
|
|
warnings.push(`Removed disallowed pattern: ${pattern.source}`);
|
|
cleaned = cleaned.replace(new RegExp(pattern.source, 'gi'), '/* [removed] */');
|
|
}
|
|
}
|
|
|
|
return { css: cleaned, warnings };
|
|
}
|
|
|
|
/**
|
|
* Validate that theme CSS only targets :root and .dark selectors.
|
|
* Returns warnings for any other selectors found.
|
|
*/
|
|
export function validateThemeSelectors(css: string): string[] {
|
|
const warnings: string[] = [];
|
|
|
|
// Remove comments
|
|
const noComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
|
|
// Find selector blocks (text before { that isn't inside a value)
|
|
const selectorRegex = /([^{}]+)\{/g;
|
|
let match;
|
|
while ((match = selectorRegex.exec(noComments)) !== null) {
|
|
const selector = match[1].trim();
|
|
// Allow :root, .dark, @font-face, @keyframes, @media
|
|
if (
|
|
selector === ':root' ||
|
|
selector === '.dark' ||
|
|
selector.startsWith('@font-face') ||
|
|
selector.startsWith('@keyframes') ||
|
|
selector.startsWith('@media') ||
|
|
selector === ''
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Inside @media blocks, also allow :root and .dark
|
|
if (selector === ':root' || selector === '.dark') continue;
|
|
|
|
warnings.push(`Non-standard selector "${selector}" — themes should only use :root and .dark`);
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
/**
|
|
* Inject theme CSS into the document head.
|
|
* Inserted after globals.css so theme variables win specificity.
|
|
*/
|
|
export function injectThemeCSS(css: string): void {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
let styleEl = document.getElementById(THEME_STYLE_ID) as HTMLStyleElement | null;
|
|
|
|
if (!styleEl) {
|
|
styleEl = document.createElement('style');
|
|
styleEl.id = THEME_STYLE_ID;
|
|
document.head.appendChild(styleEl);
|
|
}
|
|
|
|
styleEl.textContent = css;
|
|
}
|
|
|
|
/**
|
|
* Remove injected theme CSS, reverting to default.
|
|
*/
|
|
export function removeThemeCSS(): void {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const styleEl = document.getElementById(THEME_STYLE_ID);
|
|
if (styleEl) {
|
|
styleEl.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a theme CSS string is valid and safe.
|
|
*/
|
|
export function validateThemeCSSSafety(css: string): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = [];
|
|
|
|
if (!css.trim()) {
|
|
errors.push('Theme CSS is empty');
|
|
return { valid: false, errors };
|
|
}
|
|
|
|
// Check for dangerous patterns
|
|
for (const pattern of DISALLOWED_CSS_PATTERNS) {
|
|
if (pattern.test(css)) {
|
|
errors.push(`Contains disallowed pattern: ${pattern.source}`);
|
|
}
|
|
}
|
|
|
|
// Check the CSS actually sets some variables
|
|
if (!css.includes('--color-')) {
|
|
errors.push('Theme CSS should set at least one --color-* variable');
|
|
}
|
|
|
|
return { valid: errors.length === 0, errors };
|
|
}
|