@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
776 lines • 108 kB
JavaScript
/**
* SecureDownloader - Reusable utility for safe content downloads
*
* Implements the validate-before-write pattern with comprehensive security features:
* - Content validation hooks (customizable validators)
* - Atomic file operations with temp files
* - Guaranteed cleanup on failure
* - Memory-efficient streaming for large files
* - Size limits to prevent DoS attacks
* - Path validation to prevent traversal
* - Timeout handling for network operations
* - Content type validation
*
* Usage Examples:
*
* // Basic download with validation
* const downloader = new SecureDownloader();
* await downloader.downloadToFile(
* 'https://example.com/file.md',
* './downloads/file.md',
* {
* validator: async (content) => ({
* isValid: !content.includes('malicious'),
* errorMessage: content.includes('malicious') ? 'Malicious content detected' : undefined
* }),
* maxSize: 1024 * 1024, // 1MB limit
* timeout: 30000 // 30 second timeout
* }
* );
*
* // Download to memory with validation
* const content = await downloader.downloadToMemory(
* 'https://example.com/data.json',
* {
* validator: async (content) => {
* try {
* JSON.parse(content);
* return { isValid: true };
* } catch {
* return { isValid: false, errorMessage: 'Invalid JSON format' };
* }
* }
* }
* );
*
* // Streaming download for large files
* await downloader.downloadStream(
* 'https://example.com/large-file.zip',
* './downloads/large-file.zip',
* {
* streamValidator: (chunk) => !chunk.includes(Buffer.from('VIRUS')),
* maxSize: 100 * 1024 * 1024, // 100MB limit
* timeout: 300000 // 5 minute timeout
* }
* );
*/
import * as path from 'path';
import { randomBytes, createHash } from 'crypto';
import { Readable } from 'stream';
import { pipeline } from 'stream/promises';
import { createWriteStream } from 'fs';
import { SecurityError } from '../errors/SecurityError.js';
import { SECURITY_LIMITS } from '../security/constants.js';
import { ContentValidator as SecurityContentValidator } from '../security/contentValidator.js';
import { PathValidator } from '../security/pathValidator.js';
import { SecurityMonitor } from '../security/securityMonitor.js';
import { UnicodeValidator } from '../security/validators/unicodeValidator.js';
import { RateLimiter } from './RateLimiter.js';
import { logger } from './logger.js';
/**
* Custom error types for different failure scenarios
*/
export class DownloadError extends Error {
code;
originalError;
constructor(message, code, originalError) {
super(message);
this.code = code;
this.originalError = originalError;
this.name = 'DownloadError';
}
static networkError(message, originalError) {
return new DownloadError(message, 'NETWORK_ERROR', originalError);
}
static validationError(message) {
return new DownloadError(message, 'VALIDATION_ERROR');
}
static securityError(message) {
return new DownloadError(message, 'SECURITY_ERROR');
}
static timeoutError(message) {
return new DownloadError(message, 'TIMEOUT_ERROR');
}
static filesystemError(message, originalError) {
return new DownloadError(message, 'FILESYSTEM_ERROR', originalError);
}
}
/**
* SecureDownloader - Implements validate-before-write pattern for safe downloads
*
* Key Security Features:
* 1. VALIDATE-BEFORE-WRITE: All content validation occurs before any disk operations
* 2. ATOMIC OPERATIONS: Uses temporary files with atomic rename to prevent corruption
* 3. GUARANTEED CLEANUP: Automatic cleanup of temporary files on any failure
* 4. SIZE LIMITS: Prevents DoS attacks through large file downloads
* 5. PATH VALIDATION: Prevents directory traversal attacks
* 6. TIMEOUT PROTECTION: Prevents hanging network operations
* 7. CONTENT VALIDATION: Extensible validation system for different content types
*/
export class SecureDownloader {
defaultTimeout;
defaultMaxSize;
tempDir;
globalRateLimiter;
urlRateLimiters;
fileLockManager;
fileOperations;
constructor(options) {
this.defaultTimeout = options?.defaultTimeout || 30000; // 30 seconds
this.defaultMaxSize = options?.defaultMaxSize || SECURITY_LIMITS.MAX_FILE_SIZE;
this.tempDir = options?.tempDir || '.tmp';
this.fileLockManager = options?.fileLockManager ?? undefined;
this.fileOperations = options?.fileOperations ?? undefined;
// Initialize rate limiters
const rateLimitConfig = options?.rateLimitOptions || {};
this.globalRateLimiter = new RateLimiter({
maxRequests: rateLimitConfig.maxGlobalRequests || 100, // 100 downloads per hour globally
windowMs: rateLimitConfig.windowMs || 60 * 60 * 1000, // 1 hour
minDelayMs: 1000 // Minimum 1 second between requests
});
this.urlRateLimiters = new Map();
}
/**
* Download content to a file with validation
*
* SECURITY: Implements validate-before-write pattern:
* 1. Download content to memory
* 2. Validate all content
* 3. Only then write to disk atomically
*
* @param url - URL to download from
* @param destinationPath - Local file path to save to
* @param options - Download and validation options
*/
async downloadToFile(url, destinationPath, options = {}) {
const startTime = Date.now();
logger.debug(`Starting secure download from ${url} to ${destinationPath}`);
try {
// SECURITY: Validate URL and destination path first
this.validateUrl(url);
const validatedPath = await this.validateDestinationPath(destinationPath);
// SECURITY: Check if file already exists (prevent accidental overwrites)
const fileExists = await this.fileOperations.exists(validatedPath);
if (fileExists) {
throw DownloadError.filesystemError(`File already exists: ${destinationPath}`);
}
// STEP 1: Check rate limits before download
await this.checkRateLimit(url);
// STEP 2: Download content to memory (no disk operations yet)
const content = await this.downloadToMemory(url, options);
// STEP 3: Validate checksum if provided
if (options.expectedChecksum) {
await this.validateChecksum(content, options.expectedChecksum);
}
// STEP 4: All validation is complete, now write atomically
const useAtomic = options.atomic !== false; // Default to true
if (useAtomic) {
await this.atomicWriteFile(validatedPath, content);
}
else {
await this.directWriteFile(validatedPath, content);
}
const duration = Date.now() - startTime;
logger.info(`Secure download completed: ${destinationPath} (${content.length} bytes, ${duration}ms)`);
// Log successful download for security monitoring
SecurityMonitor.logSecurityEvent({
type: 'FILE_COPIED',
severity: 'LOW',
source: 'SecureDownloader',
details: `Downloaded ${content.length} bytes from ${url} to ${destinationPath}`,
metadata: {
url,
destinationPath,
contentLength: content.length,
duration
}
});
}
catch (error) {
const duration = Date.now() - startTime;
logger.error(`Secure download failed: ${error instanceof Error ? error.message : String(error)}`);
// Log failed download for security monitoring
SecurityMonitor.logSecurityEvent({
type: 'PATH_TRAVERSAL_ATTEMPT',
severity: 'MEDIUM',
source: 'SecureDownloader',
details: `Download failed: ${error instanceof Error ? error.message : String(error)}`,
metadata: {
url,
destinationPath,
duration,
errorType: error instanceof DownloadError ? error.code : 'UNKNOWN'
}
});
throw error;
}
}
/**
* Download content to memory with validation
*
* @param url - URL to download from
* @param options - Download and validation options
* @returns Validated content as string
*/
async downloadToMemory(url, options = {}) {
const timeout = options.timeout || this.defaultTimeout;
const maxSize = options.maxSize || this.defaultMaxSize;
logger.debug(`Downloading content from ${url} (max: ${maxSize} bytes, timeout: ${timeout}ms)`);
try {
// SECURITY: Validate URL format
this.validateUrl(url);
// STEP 1: Check rate limits before download
await this.checkRateLimit(url);
// STEP 2: Fetch content with size and timeout protection
const content = await this.fetchWithLimits(url, maxSize, timeout, options.headers);
// STEP 3: Validate content type if specified
if (options.expectedContentType) {
await this.validateContentType(content, options.expectedContentType);
}
// STEP 4: Validate checksum if provided
if (options.expectedChecksum) {
await this.validateChecksum(content, options.expectedChecksum);
}
// STEP 5: Run built-in security validation
const securityResult = SecurityContentValidator.validateAndSanitize(content);
if (!securityResult.isValid && securityResult.severity === 'critical') {
throw DownloadError.securityError(`Critical security threat detected: ${securityResult.detectedPatterns?.join(', ')}`);
}
// STEP 6: Run custom validator if provided
if (options.validator) {
logger.debug('Running custom content validation');
const validationResult = await options.validator(content);
if (!validationResult.isValid) {
throw DownloadError.validationError(validationResult.errorMessage || 'Content validation failed');
}
}
logger.debug(`Content validation passed (${content.length} bytes)`);
return securityResult.sanitizedContent || content;
}
catch (error) {
if (error instanceof DownloadError) {
throw error;
}
throw DownloadError.networkError(`Failed to download content from ${url}: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
}
}
/**
* Download large files using streaming with chunk-level validation
*
* @param url - URL to download from
* @param destinationPath - Local file path to save to
* @param options - Streaming download options
*/
async downloadStream(url, destinationPath, options = {}) {
const startTime = Date.now();
const maxSize = options.maxSize || this.defaultMaxSize;
const timeout = options.timeout || this.defaultTimeout;
logger.debug(`Starting streaming download from ${url} to ${destinationPath}`);
try {
// SECURITY: Check rate limits before download
await this.checkRateLimit(url);
// SECURITY: Validate URL and destination path
this.validateUrl(url);
const validatedPath = await this.validateDestinationPath(destinationPath);
// Generate temporary file path for atomic operation
const tempPath = await this.getTempFilePath(validatedPath);
let downloadedSize = 0;
let timeoutHandle;
// Track write stream for cleanup (declared outside try for catch block access)
let writeStream;
let writeStreamClosed = false;
// Create abort controller for timeout handling
const abortController = new AbortController();
timeoutHandle = setTimeout(() => {
abortController.abort();
}, timeout);
try {
// SECURITY: Fetch with abort signal for timeout
const response = await fetch(url, {
signal: abortController.signal,
headers: options.headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
if (!response.body) {
throw new Error('Response body is null');
}
// Ensure temp directory exists
await this.fileOperations.createDirectory(path.dirname(tempPath));
// Create write stream to temporary file
writeStream = createWriteStream(tempPath);
// Track whether write stream has been closed for cleanup
writeStream.on('close', () => {
writeStreamClosed = true;
});
// Create a transform stream for validation and size checking
const validationStream = new Readable({
async read() {
// This stream will be fed by the pipeline
}
});
// Set up chunk validation and size checking
const reader = response.body.getReader();
const pump = async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
// SECURITY: Check size limit
downloadedSize += value.length;
if (downloadedSize > maxSize) {
throw DownloadError.securityError(`File size exceeds limit: ${downloadedSize} > ${maxSize} bytes`);
}
// SECURITY: Run chunk validator if provided
if (options.streamValidator && !options.streamValidator(value)) {
throw DownloadError.validationError('Chunk validation failed');
}
validationStream.push(value);
}
validationStream.push(null); // End stream
}
catch (error) {
validationStream.destroy(error instanceof Error ? error : new Error(String(error)));
}
};
// Start the pump and pipeline concurrently
await Promise.all([
pump(),
pipeline(validationStream, writeStream)
]);
// Clear timeout
if (timeoutHandle) {
clearTimeout(timeoutHandle);
timeoutHandle = undefined;
}
// SECURITY: Atomic rename to final destination
await this.fileOperations.renameFile(tempPath, validatedPath);
const duration = Date.now() - startTime;
logger.info(`Streaming download completed: ${destinationPath} (${downloadedSize} bytes, ${duration}ms)`);
// Log successful streaming download
SecurityMonitor.logSecurityEvent({
type: 'FILE_COPIED',
severity: 'LOW',
source: 'SecureDownloader',
details: `Streamed ${downloadedSize} bytes from ${url} to ${destinationPath}`,
metadata: {
url,
destinationPath,
contentLength: downloadedSize,
duration
}
});
}
catch (error) {
// SECURITY: Ensure write stream is closed before cleanup to release file handle
// This is critical on macOS where unlink can fail or leave stale directory entries
// if the file handle is still open
if (writeStream && !writeStreamClosed) {
await new Promise((resolve) => {
writeStream.destroy();
writeStream.once('close', () => resolve());
// Safety timeout in case close event never fires
setTimeout(() => resolve(), 100);
});
}
// SECURITY: Guaranteed cleanup of temporary file
try {
await this.fileOperations.deleteFile(tempPath, undefined, { source: 'SecureDownloader.downloadStream' });
logger.debug(`Cleaned up temp file: ${tempPath}`);
}
catch (cleanupError) {
logger.warn(`Failed to clean up temp file ${tempPath}: ${cleanupError}`);
}
throw error;
}
finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
}
catch (error) {
const duration = Date.now() - startTime;
logger.error(`Streaming download failed: ${error instanceof Error ? error.message : String(error)}`);
// Log failed streaming download
SecurityMonitor.logSecurityEvent({
type: 'PATH_TRAVERSAL_ATTEMPT',
severity: 'MEDIUM',
source: 'SecureDownloader',
details: `Streaming download failed: ${error instanceof Error ? error.message : String(error)}`,
metadata: {
url,
destinationPath,
duration,
errorType: error instanceof DownloadError ? error.code : 'UNKNOWN'
}
});
if (error instanceof Error && error.name === 'AbortError') {
throw DownloadError.timeoutError(`Download timed out after ${timeout}ms`);
}
if (error instanceof DownloadError) {
throw error;
}
throw DownloadError.networkError(`Streaming download failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined);
}
}
/**
* Validate URL format and security with Unicode normalization
*/
validateUrl(url) {
if (!url || typeof url !== 'string') {
throw DownloadError.validationError('URL must be a non-empty string');
}
// SECURITY FIX: DMCP-SEC-004 - Unicode normalization on user input
const unicodeValidation = UnicodeValidator.normalize(url);
const normalizedUrl = unicodeValidation.normalizedContent;
if (!unicodeValidation.isValid) {
SecurityMonitor.logSecurityEvent({
type: 'UNICODE_VALIDATION_ERROR',
severity: 'MEDIUM',
source: 'SecureDownloader',
details: `URL contains suspicious Unicode patterns: ${unicodeValidation.detectedIssues?.join(', ')}`,
metadata: { originalUrl: url, normalizedUrl }
});
}
// Use normalized URL for further validation
url = normalizedUrl;
let parsedUrl;
try {
parsedUrl = new URL(url);
}
catch {
throw DownloadError.validationError(`Invalid URL format: ${url}`);
}
// SECURITY: Only allow HTTPS and HTTP protocols
if (!['https:', 'http:'].includes(parsedUrl.protocol)) {
throw DownloadError.securityError(`Unsupported protocol: ${parsedUrl.protocol}. Only HTTP/HTTPS allowed.`);
}
// SECURITY: Prevent requests to localhost/private networks
const hostname = parsedUrl.hostname.toLowerCase();
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
throw DownloadError.securityError('Downloads from localhost are not allowed');
}
// SECURITY: Check for private IP ranges (basic protection)
if (hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.startsWith('172.')) {
throw DownloadError.securityError('Downloads from private IP ranges are not allowed');
}
}
/**
* Validate destination path for security
*/
async validateDestinationPath(filePath) {
try {
// Use existing PathValidator for comprehensive path validation
return await PathValidator.validatePersonaPath(filePath);
}
catch (error) {
throw DownloadError.securityError(`Invalid destination path: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Fetch content with size and timeout limits
*/
async fetchWithLimits(url, maxSize, timeout, headers) {
const abortController = new AbortController();
const timeoutHandle = setTimeout(() => abortController.abort(), timeout);
try {
const response = await fetch(url, {
signal: abortController.signal,
headers: {
'User-Agent': 'DollhouseMCP-SecureDownloader/1.0',
...headers
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// SECURITY: Check Content-Length header if available
const contentLength = response.headers.get('content-length');
if (contentLength && Number.parseInt(contentLength, 10) > maxSize) {
throw DownloadError.securityError(`Content size ${contentLength} exceeds limit of ${maxSize} bytes`);
}
// Read content with size checking
const chunks = [];
let totalSize = 0;
if (!response.body) {
throw new Error('Response body is null');
}
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
totalSize += value.length;
if (totalSize > maxSize) {
throw DownloadError.securityError(`Content size ${totalSize} exceeds limit of ${maxSize} bytes`);
}
chunks.push(value);
}
}
finally {
reader.releaseLock();
}
// Combine chunks and decode
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const combined = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
combined.set(chunk, offset);
offset += chunk.length;
}
return new TextDecoder('utf-8').decode(combined);
}
catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw DownloadError.timeoutError(`Request timed out after ${timeout}ms`);
}
throw error;
}
finally {
clearTimeout(timeoutHandle);
}
}
/**
* Validate content type if specified
*/
async validateContentType(content, expectedType) {
// Basic content type validation based on content analysis
switch (expectedType.toLowerCase()) {
case 'json':
try {
JSON.parse(content);
}
catch {
throw DownloadError.validationError('Content is not valid JSON');
}
break;
case 'yaml':
case 'yml':
// Use existing YAML validation
if (!SecurityContentValidator.validateYamlContent(content)) {
throw DownloadError.validationError('Content is not valid YAML');
}
break;
case 'markdown':
case 'md':
// Basic markdown validation (check for frontmatter format)
if (content.startsWith('---')) {
const frontmatterEnd = content.indexOf('\n---\n', 3);
if (frontmatterEnd === -1) {
throw DownloadError.validationError('Invalid markdown frontmatter format');
}
}
break;
default:
logger.debug(`No specific validation for content type: ${expectedType}`);
}
}
/**
* Atomic file write using FileOperationsService
*/
async atomicWriteFile(filePath, content) {
// Ensure directory exists
await this.fileOperations.createDirectory(path.dirname(filePath));
// Use FileOperationsService's atomic write
await this.fileOperations.writeFile(filePath, content, {
source: 'SecureDownloader.atomicWriteFile',
atomic: true
});
}
/**
* Direct file write (non-atomic, for when atomic is disabled)
*/
async directWriteFile(filePath, content) {
// Ensure directory exists
await this.fileOperations.createDirectory(path.dirname(filePath));
// Direct write
await this.fileOperations.writeFile(filePath, content, {
source: 'SecureDownloader.directWriteFile',
atomic: false
});
}
/**
* Generate temporary file path for atomic operations
*/
async getTempFilePath(originalPath) {
const dir = path.dirname(originalPath);
const basename = path.basename(originalPath);
const random = randomBytes(8).toString('hex');
const tempDir = path.join(dir, this.tempDir);
// Ensure temp directory exists
await this.fileOperations.createDirectory(tempDir);
return path.join(tempDir, `${basename}.${random}.tmp`);
}
/**
* Check rate limits for downloads
*/
async checkRateLimit(url) {
// Check global rate limit
const globalStatus = this.globalRateLimiter.checkLimit();
if (!globalStatus.allowed) {
SecurityMonitor.logSecurityEvent({
type: 'RATE_LIMIT_EXCEEDED',
severity: 'MEDIUM',
source: 'SecureDownloader',
details: `Global download rate limit exceeded. Retry after ${globalStatus.retryAfterMs}ms`,
metadata: { url, retryAfterMs: globalStatus.retryAfterMs }
});
throw DownloadError.securityError(`Download rate limit exceeded. Please retry after ${Math.ceil(globalStatus.retryAfterMs / 1000)} seconds`);
}
// Check per-URL rate limit
const parsedUrl = new URL(url);
const urlKey = `${parsedUrl.hostname}:${parsedUrl.port || (parsedUrl.protocol === 'https:' ? '443' : '80')}`;
if (!this.urlRateLimiters.has(urlKey)) {
this.urlRateLimiters.set(urlKey, new RateLimiter({
maxRequests: 10, // 10 requests per hour per URL
windowMs: 60 * 60 * 1000,
minDelayMs: 5000 // 5 second minimum delay between requests to same URL
}));
}
const urlLimiter = this.urlRateLimiters.get(urlKey);
const urlStatus = urlLimiter.checkLimit();
if (!urlStatus.allowed) {
SecurityMonitor.logSecurityEvent({
type: 'RATE_LIMIT_EXCEEDED',
severity: 'MEDIUM',
source: 'SecureDownloader',
details: `Per-URL download rate limit exceeded for ${urlKey}. Retry after ${urlStatus.retryAfterMs}ms`,
metadata: { url, urlKey, retryAfterMs: urlStatus.retryAfterMs }
});
throw DownloadError.securityError(`Too many requests to ${urlKey}. Please retry after ${Math.ceil(urlStatus.retryAfterMs / 1000)} seconds`);
}
// Consume rate limit tokens
this.globalRateLimiter.consumeToken();
urlLimiter.consumeToken();
}
/**
* Validate content checksum for integrity verification
*/
async validateChecksum(content, expectedChecksum) {
const normalizedExpected = expectedChecksum.toLowerCase().trim();
// Validate checksum format (SHA-256 should be 64 hex characters)
if (!/^[a-f0-9]{64}$/.test(normalizedExpected)) {
throw DownloadError.validationError('Invalid checksum format. Expected SHA-256 (64 hex characters)');
}
const contentBuffer = Buffer.from(content, 'utf-8');
const actualChecksum = createHash('sha256').update(contentBuffer).digest('hex');
if (actualChecksum !== normalizedExpected) {
SecurityMonitor.logSecurityEvent({
type: 'CONTENT_INJECTION_ATTEMPT',
severity: 'HIGH',
source: 'SecureDownloader',
details: `Checksum mismatch detected - possible content tampering`,
metadata: {
expectedChecksum: normalizedExpected,
actualChecksum,
contentLength: content.length
}
});
throw DownloadError.securityError(`Content checksum verification failed. Expected: ${normalizedExpected}, Got: ${actualChecksum}`);
}
logger.debug(`Checksum validation passed: ${actualChecksum}`);
}
/**
* Create a content validator that combines multiple validators
*/
static combineValidators(...validators) {
return async (content) => {
for (const validator of validators) {
const result = await validator(content);
if (!result.isValid) {
return result;
}
}
return { isValid: true };
};
}
/**
* Create a content validator for JSON content
*/
static jsonValidator() {
return async (content) => {
try {
JSON.parse(content);
return { isValid: true };
}
catch (error) {
return {
isValid: false,
errorMessage: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
severity: 'medium'
};
}
};
}
/**
* Create a content validator for YAML content
*/
static yamlValidator() {
return async (content) => {
const isValid = SecurityContentValidator.validateYamlContent(content);
return {
isValid,
errorMessage: isValid ? undefined : 'Invalid or malicious YAML content',
severity: isValid ? 'low' : 'high'
};
};
}
/**
* Create a content validator for markdown content
*/
static markdownValidator() {
return async (content) => {
try {
// Use existing persona content sanitization for markdown
SecurityContentValidator.sanitizePersonaContent(content);
return { isValid: true };
}
catch (error) {
return {
isValid: false,
errorMessage: `Invalid markdown: ${error instanceof Error ? error.message : String(error)}`,
severity: error instanceof SecurityError ? 'critical' : 'medium'
};
}
};
}
/**
* Create a content validator with size limits
*/
static sizeValidator(maxSize) {
return async (content) => {
const size = Buffer.byteLength(content, 'utf-8');
if (size > maxSize) {
return {
isValid: false,
errorMessage: `Content size ${size} exceeds limit of ${maxSize} bytes`,
severity: 'high'
};
}
return { isValid: true };
};
}
/**
* Create a content validator that checks for forbidden patterns
*/
static patternValidator(forbiddenPatterns, errorMessage = 'Forbidden pattern detected') {
return async (content) => {
for (const pattern of forbiddenPatterns) {
if (pattern.test(content)) {
return {
isValid: false,
errorMessage,
severity: 'high',
metadata: { pattern: pattern.source }
};
}
}
return { isValid: true };
};
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiU2VjdXJlRG93bmxvYWRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9TZWN1cmVEb3dubG9hZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBdURHO0FBRUgsT0FBTyxLQUFLLElBQUksTUFBTSxNQUFNLENBQUM7QUFDN0IsT0FBTyxFQUFFLFdBQVcsRUFBRSxVQUFVLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFDakQsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLFFBQVEsQ0FBQztBQUNsQyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDM0MsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sSUFBSSxDQUFDO0FBRXZDLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUMzRCxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDM0QsT0FBTyxFQUFFLGdCQUFnQixJQUFJLHdCQUF3QixFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDL0YsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBRTdELE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUNqRSxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSw0Q0FBNEMsQ0FBQztBQUM5RSxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDL0MsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGFBQWEsQ0FBQztBQTZEckM7O0dBRUc7QUFDSCxNQUFNLE9BQU8sYUFBYyxTQUFRLEtBQUs7SUFHcEI7SUFDQTtJQUhsQixZQUNFLE9BQWUsRUFDQyxJQUFZLEVBQ1osYUFBcUI7UUFFckMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBSEMsU0FBSSxH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