anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
581 lines • 21.9 kB
JavaScript
;
/**
* 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, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
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