UNPKG

anon-identity

Version:

Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure

581 lines 21.9 kB
"use strict"; /** * Result Validator for MCP Functions * * Validates and sanitizes function execution results */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ResultValidator = exports.VALIDATION_PATTERNS = void 0; const types_1 = require("../types"); /** * Built-in validation patterns */ exports.VALIDATION_PATTERNS = { EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, URL: /^https?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?$/, UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, PHONE: /^\+?[\d\s\-\(\)]{7,}$/, IP_ADDRESS: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, DATE_ISO: /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2}))?$/, ALPHANUMERIC: /^[a-zA-Z0-9]+$/, NO_SQL_INJECTION: /^(?!.*(?:'|"|;|--|\*|\/\*|\*\/|xp_|sp_|union|select|insert|update|delete|drop|create|alter|exec|execute)).+$/i, NO_XSS: /^(?!.*(?:<script|javascript:|onclick|onload|onerror|onmouseover)).+$/i }; /** * Result Validator */ class ResultValidator { constructor(defaultSanitizationOptions = { removeUnknownProperties: true, truncateStrings: true, maxStringLength: 10000, maxArrayLength: 1000, maxObjectDepth: 10, convertTypes: false, removeNullValues: false, escapeSqlInjection: true, escapeXssVectors: true, removeScriptTags: true }) { this.defaultSanitizationOptions = defaultSanitizationOptions; this.schemas = new Map(); this.customRules = new Map(); this.initializeBuiltinSchemas(); } /** * Register validation schema for a function */ registerSchema(functionName, schema) { this.schemas.set(functionName, schema); } /** * Register custom validation rule */ registerCustomRule(ruleName, validator) { this.customRules.set(ruleName, validator); } /** * Validate function result */ async validateResult(functionName, result, options) { const startTime = Date.now(); const schema = this.schemas.get(functionName); if (!schema) { // No schema defined, perform basic sanitization only return this.basicValidation(result, options); } const sanitizationOptions = { ...this.defaultSanitizationOptions, ...options }; const errors = []; const warnings = []; const transformations = []; let rulesApplied = 0; try { const sanitizedValue = await this.validateValue(result, schema, '', errors, warnings, transformations, sanitizationOptions, 0); rulesApplied = this.countRules(schema); return { valid: errors.length === 0, errors, warnings, sanitizedValue: errors.length === 0 ? sanitizedValue : undefined, metadata: { validatedAt: new Date(), validationTime: Date.now() - startTime, rulesApplied, transformationsApplied: transformations } }; } catch (error) { throw new types_1.MCPError({ code: types_1.MCPErrorCode.FUNCTION_ERROR, message: `Validation failed: ${error.message}`, timestamp: new Date(), retryable: false }); } } /** * Validate value against schema */ async validateValue(value, schema, path, errors, warnings, transformations, options, depth) { // Check maximum depth if (depth > options.maxObjectDepth) { errors.push({ path, rule: 'custom', message: `Maximum object depth exceeded: ${depth} > ${options.maxObjectDepth}`, actualValue: value, expectedConstraint: options.maxObjectDepth }); return value; } // Type validation and conversion let sanitizedValue = value; if (schema.type !== 'any') { const typeResult = this.validateType(value, schema.type, path); if (!typeResult.valid) { if (options.convertTypes) { const converted = this.attemptTypeConversion(value, schema.type); if (converted.success) { sanitizedValue = converted.value; transformations.push(`${path}: converted ${typeof value} to ${schema.type}`); } else { errors.push({ path, rule: 'type', message: typeResult.message, actualValue: value, expectedConstraint: schema.type }); return value; } } else { errors.push({ path, rule: 'type', message: typeResult.message, actualValue: value, expectedConstraint: schema.type }); return value; } } } // Apply validation rules for (const rule of schema.rules) { const ruleResult = await this.applyValidationRule(sanitizedValue, rule, path); if (ruleResult.transformed) { sanitizedValue = ruleResult.value; transformations.push(`${path}: ${rule.type} rule applied`); } if (!ruleResult.valid) { if (rule.severity === 'error') { errors.push({ path, rule: rule.type, message: rule.message || ruleResult.message || 'Validation failed', actualValue: value, expectedConstraint: rule.constraint }); } else if (rule.severity === 'warning') { warnings.push({ path, rule: rule.type, message: rule.message || ruleResult.message || 'Validation warning', suggestion: ruleResult.suggestion }); } } } // Type-specific validation switch (schema.type) { case 'string': sanitizedValue = this.validateString(sanitizedValue, path, errors, warnings, transformations, options); break; case 'array': sanitizedValue = await this.validateArray(sanitizedValue, schema, path, errors, warnings, transformations, options, depth); break; case 'object': sanitizedValue = await this.validateObject(sanitizedValue, schema, path, errors, warnings, transformations, options, depth); break; } return sanitizedValue; } /** * Validate type */ validateType(value, expectedType, path) { const actualType = Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value; if (actualType === expectedType) { return { valid: true, message: '' }; } return { valid: false, message: `Expected ${expectedType}, got ${actualType} at ${path}` }; } /** * Attempt type conversion */ attemptTypeConversion(value, targetType) { try { switch (targetType) { case 'string': return { success: true, value: String(value) }; case 'number': const num = Number(value); return { success: !isNaN(num), value: num }; case 'boolean': if (typeof value === 'string') { const lower = value.toLowerCase(); if (lower === 'true' || lower === '1') return { success: true, value: true }; if (lower === 'false' || lower === '0') return { success: true, value: false }; } return { success: true, value: Boolean(value) }; case 'array': return Array.isArray(value) ? { success: true, value } : { success: false, value }; default: return { success: false, value }; } } catch { return { success: false, value }; } } /** * Apply validation rule */ async applyValidationRule(value, rule, path) { switch (rule.type) { case 'length': return this.applyLengthRule(value, rule.constraint, path); case 'range': return this.applyRangeRule(value, rule.constraint, path); case 'pattern': return this.applyPatternRule(value, rule.constraint, path); case 'whitelist': return this.applyWhitelistRule(value, rule.constraint, path); case 'blacklist': return this.applyBlacklistRule(value, rule.constraint, path); case 'format': return this.applyFormatRule(value, rule.constraint, path); case 'custom': return this.applyCustomRule(value, rule.constraint, path); default: return { valid: true, value, transformed: false }; } } /** * Apply length rule */ applyLengthRule(value, constraint, path) { const length = typeof value === 'string' ? value.length : Array.isArray(value) ? value.length : typeof value === 'object' ? Object.keys(value).length : 0; let valid = true; let message = ''; let transformedValue = value; let transformed = false; if (constraint.min !== undefined && length < constraint.min) { valid = false; message = `Length ${length} is less than minimum ${constraint.min} at ${path}`; } if (constraint.max !== undefined && length > constraint.max) { if (typeof value === 'string') { transformedValue = value.substring(0, constraint.max); transformed = true; } else if (Array.isArray(value)) { transformedValue = value.slice(0, constraint.max); transformed = true; } else { valid = false; message = `Length ${length} exceeds maximum ${constraint.max} at ${path}`; } } return { valid: valid || transformed, value: transformedValue, transformed, message }; } /** * Apply range rule */ applyRangeRule(value, constraint, path) { if (typeof value !== 'number') { return { valid: false, value, transformed: false, message: `Range rule applied to non-number at ${path}` }; } let valid = true; let message = ''; let transformedValue = value; let transformed = false; if (constraint.min !== undefined && value < constraint.min) { transformedValue = constraint.min; transformed = true; } if (constraint.max !== undefined && value > constraint.max) { transformedValue = constraint.max; transformed = true; } return { valid, value: transformedValue, transformed, message }; } /** * Apply pattern rule */ applyPatternRule(value, pattern, path) { if (typeof value !== 'string') { return { valid: false, value, transformed: false, message: `Pattern rule applied to non-string at ${path}` }; } const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern; const valid = regex.test(value); return { valid, value, transformed: false, message: valid ? undefined : `Value does not match pattern ${pattern} at ${path}` }; } /** * Apply whitelist rule */ applyWhitelistRule(value, allowedValues, path) { const valid = allowedValues.includes(value); return { valid, value, transformed: false, message: valid ? undefined : `Value not in whitelist at ${path}` }; } /** * Apply blacklist rule */ applyBlacklistRule(value, forbiddenValues, path) { const valid = !forbiddenValues.includes(value); return { valid, value, transformed: false, message: valid ? undefined : `Value in blacklist at ${path}` }; } /** * Apply format rule */ applyFormatRule(value, format, path) { if (typeof value !== 'string') { return { valid: false, value, transformed: false, message: `Format rule applied to non-string at ${path}` }; } const pattern = exports.VALIDATION_PATTERNS[format]; if (!pattern) { return { valid: false, value, transformed: false, message: `Unknown format: ${format}` }; } const valid = pattern.test(value); return { valid, value, transformed: false, message: valid ? undefined : `Value does not match format ${format} at ${path}` }; } /** * Apply custom rule */ applyCustomRule(value, ruleName, path) { const customValidator = this.customRules.get(ruleName); if (!customValidator) { return { valid: false, value, transformed: false, message: `Unknown custom rule: ${ruleName}` }; } const valid = customValidator(value); return { valid, value, transformed: false, message: valid ? undefined : `Custom rule ${ruleName} failed at ${path}` }; } /** * Validate string */ validateString(value, path, errors, warnings, transformations, options) { let sanitized = value; // Truncate if too long if (options.truncateStrings && sanitized.length > options.maxStringLength) { sanitized = sanitized.substring(0, options.maxStringLength); transformations.push(`${path}: truncated to ${options.maxStringLength} characters`); } // Remove script tags if (options.removeScriptTags) { const original = sanitized; sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); if (original !== sanitized) { transformations.push(`${path}: removed script tags`); } } // Escape XSS vectors if (options.escapeXssVectors) { const original = sanitized; sanitized = sanitized .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#x27;') .replace(/\//g, '&#x2F;'); if (original !== sanitized) { transformations.push(`${path}: escaped XSS vectors`); } } // Check for SQL injection patterns if (options.escapeSqlInjection && !exports.VALIDATION_PATTERNS.NO_SQL_INJECTION.test(sanitized)) { warnings.push({ path, rule: 'pattern', message: 'Potential SQL injection pattern detected', suggestion: 'Consider using parameterized queries' }); } return sanitized; } /** * Validate array */ async validateArray(value, schema, path, errors, warnings, transformations, options, depth) { let sanitized = [...value]; // Truncate if too long if (sanitized.length > options.maxArrayLength) { sanitized = sanitized.slice(0, options.maxArrayLength); transformations.push(`${path}: truncated to ${options.maxArrayLength} items`); } // Validate items if (schema.items) { const validatedItems = []; for (let i = 0; i < sanitized.length; i++) { const itemPath = `${path}[${i}]`; const validatedItem = await this.validateValue(sanitized[i], schema.items, itemPath, errors, warnings, transformations, options, depth + 1); validatedItems.push(validatedItem); } sanitized = validatedItems; } return sanitized; } /** * Validate object */ async validateObject(value, schema, path, errors, warnings, transformations, options, depth) { let sanitized = { ...value }; // Check required properties if (schema.required) { for (const requiredProp of schema.required) { if (!(requiredProp in sanitized)) { errors.push({ path: `${path}.${requiredProp}`, rule: 'type', message: `Missing required property: ${requiredProp}`, actualValue: undefined, expectedConstraint: 'required' }); } } } // Remove unknown properties if configured if (options.removeUnknownProperties && schema.properties && !schema.additionalProperties) { const knownProps = new Set([ ...Object.keys(schema.properties), ...(schema.optional || []) ]); for (const prop of Object.keys(sanitized)) { if (!knownProps.has(prop)) { delete sanitized[prop]; transformations.push(`${path}.${prop}: removed unknown property`); } } } // Validate known properties if (schema.properties) { for (const [prop, propSchema] of Object.entries(schema.properties)) { if (prop in sanitized) { const propPath = `${path}.${prop}`; sanitized[prop] = await this.validateValue(sanitized[prop], propSchema, propPath, errors, warnings, transformations, options, depth + 1); } } } // Remove null values if configured if (options.removeNullValues) { for (const [prop, propValue] of Object.entries(sanitized)) { if (propValue === null) { delete sanitized[prop]; transformations.push(`${path}.${prop}: removed null value`); } } } return sanitized; } /** * Basic validation for functions without schemas */ async basicValidation(result, options) { const startTime = Date.now(); const sanitizationOptions = { ...this.defaultSanitizationOptions, ...options }; const transformations = []; let sanitizedValue = result; // Basic sanitization if (typeof result === 'string') { sanitizedValue = this.validateString(result, 'result', [], [], transformations, sanitizationOptions); } return { valid: true, errors: [], warnings: [], sanitizedValue, metadata: { validatedAt: new Date(), validationTime: Date.now() - startTime, rulesApplied: 0, transformationsApplied: transformations } }; } /** * Count rules in schema */ countRules(schema) { let count = schema.rules.length; if (schema.properties) { for (const propSchema of Object.values(schema.properties)) { count += this.countRules(propSchema); } } if (schema.items) { count += this.countRules(schema.items); } return count; } /** * Initialize built-in schemas */ initializeBuiltinSchemas() { // Common data validation schemas this.registerSchema('validate_email', { type: 'string', rules: [ { type: 'format', constraint: 'EMAIL', severity: 'error' } ] }); this.registerSchema('validate_url', { type: 'string', rules: [ { type: 'format', constraint: 'URL', severity: 'error' } ] }); this.registerSchema('safe_text', { type: 'string', rules: [ { type: 'pattern', constraint: exports.VALIDATION_PATTERNS.NO_XSS, severity: 'error' }, { type: 'pattern', constraint: exports.VALIDATION_PATTERNS.NO_SQL_INJECTION, severity: 'warning' }, { type: 'length', constraint: { max: 1000 }, severity: 'warning' } ] }); } /** * Get registered schemas */ getRegisteredSchemas() { return Array.from(this.schemas.keys()); } /** * Remove schema */ removeSchema(functionName) { return this.schemas.delete(functionName); } } exports.ResultValidator = ResultValidator; exports.default = ResultValidator; //# sourceMappingURL=result-validator.js.map