UNPKG

fortify-schema

Version:

A modern TypeScript validation library designed around familiar interface syntax and powerful conditional validation. Experience schema validation that feels natural to TypeScript developers while unlocking advanced runtime validation capabilities.

342 lines (301 loc) 9.9 kB
import { ErrorHandler } from "../schema/mode/interfaces/errors/ErrorHandler"; import { ErrorCode } from "../schema/mode/interfaces/errors/types/errors.type"; import { ValidationError } from "../types/types"; // Helper export function for deep JSON validation export function validateJsonDeep( data: any, options: { maxDepth: number; maxKeys: number; maxStringLength: number; maxArrayLength: number; allowedTypes: string[]; forbiddenKeys: string[]; currentDepth: number; keyCount: number; } ): { success: boolean; errors: ValidationError[]; warnings: string[] } { const result = { success: true, errors: [] as ValidationError[], warnings: [] as string[], }; if (options.currentDepth > options.maxDepth) { result.success = false; result.errors.push( ErrorHandler.createError( [], `Maximum depth of ${options.maxDepth} exceeded`, ErrorCode.SECURITY_VIOLATION, "valid depth", options.currentDepth ) ); return result; } const dataType = Array.isArray(data) ? "array" : data === null ? "null" : typeof data; if (!options.allowedTypes.includes(dataType)) { result.success = false; result.errors.push( ErrorHandler.createSimpleError( `Type '${dataType}' is not allowed`, ErrorCode.SECURITY_VIOLATION ) ); return result; } if (typeof data === "string" && data.length > options.maxStringLength) { result.success = false; result.errors.push( ErrorHandler.createError( [], `String length exceeds maximum of ${options.maxStringLength}`, ErrorCode.SECURITY_VIOLATION, `string with length <= ${options.maxStringLength}`, data.length ) ); } if (Array.isArray(data)) { if (data.length > options.maxArrayLength) { result.success = false; result.errors.push( ErrorHandler.createError( [], `Array length exceeds maximum of ${options.maxArrayLength}`, ErrorCode.SECURITY_VIOLATION, `array with length <= ${options.maxArrayLength}`, data.length ) ); } for (const item of data) { const itemResult = validateJsonDeep(item, { ...options, currentDepth: options.currentDepth + 1, }); result.success = result.success && itemResult.success; result.errors.push(...itemResult.errors); result.warnings.push(...itemResult.warnings); } } if (typeof data === "object" && data !== null && !Array.isArray(data)) { const keys = Object.keys(data); options.keyCount += keys.length; if (options.keyCount > options.maxKeys) { result.success = false; result.errors.push( ErrorHandler.createError( [], `Maximum key count of ${options.maxKeys} exceeded`, ErrorCode.SECURITY_VIOLATION, `object with <= ${options.maxKeys} keys`, options.keyCount ) ); } for (const key of keys) { if (options.forbiddenKeys.includes(key)) { result.success = false; result.errors.push( ErrorHandler.createError( [key], `Forbidden key '${key}' found`, ErrorCode.SECURITY_VIOLATION, "allowed key", key ) ); } const valueResult = validateJsonDeep(data[key], { ...options, currentDepth: options.currentDepth + 1, }); result.success = result.success && valueResult.success; result.errors.push(...valueResult.errors); result.warnings.push(...valueResult.warnings); } } return result; } // Helper export function for JSON schema validation export function validateJsonSchema( data: any, schema: any ): { success: boolean; errors: ValidationError[]; warnings: string[] } { // Basic schema validation - you might want to use a proper JSON schema library like Ajv const result = { success: true, errors: [] as ValidationError[], warnings: [] as string[], }; if (schema.type) { const dataType = Array.isArray(data) ? "array" : data === null ? "null" : typeof data; if (dataType !== schema.type) { result.success = false; result.errors.push( ErrorHandler.createError( [], `Expected type '${schema.type}', got '${dataType}'`, ErrorCode.TYPE_MISMATCH, schema.type, data ) ); } } if (schema.properties && typeof data === "object" && data !== null) { for (const [key, propSchema] of Object.entries(schema.properties)) { if (key in data) { const propResult = validateJsonSchema(data[key], propSchema); result.success = result.success && propResult.success; result.errors.push(...propResult.errors); result.warnings.push(...propResult.warnings); } } } return result; } // Helper export function for IPv4 validation export function validateIPv4(ip: string, strict: boolean): IPValidationResult { const ipv4Regex = strict ? /^(?:(?: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]?)$/ : /^(?:\d{1,3}\.){3}\d{1,3}$/; if (!ipv4Regex.test(ip)) { return { valid: false }; } if (strict) { const octets = ip.split("."); // Check for leading zeros (except for "0" itself) for (const octet of octets) { if (octet.length > 1 && octet.startsWith("0")) { return { valid: false }; // Leading zeros not allowed } const num = Number(octet); if (num < 0 || num > 255) { return { valid: false }; } } } return { valid: true, normalized: ip }; } // Helper export function for IPv6 validation export function validateIPv6(ip: string, strict: boolean): IPValidationResult { // Improved IPv6 regex that properly handles compressed notation const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; if (!ipv6Regex.test(ip)) { return { valid: false }; } return { valid: true, normalized: normalizeIPv6(ip) }; } // Helper export function for IPv6 normalization export function normalizeIPv6(ip: string): string { // Expand compressed notation and normalize to full form if (ip === "::") { return "0000:0000:0000:0000:0000:0000:0000:0000"; } if (ip === "::1") { return "0000:0000:0000:0000:0000:0000:0000:0001"; } // Handle compressed notation if (ip.includes("::")) { const parts = ip.split("::"); const leftParts = parts[0] ? parts[0].split(":") : []; const rightParts = parts[1] ? parts[1].split(":") : []; const missingParts = 8 - leftParts.length - rightParts.length; const fullParts = [ ...leftParts, ...Array(missingParts).fill("0000"), ...rightParts, ]; return fullParts.map((part) => part.padStart(4, "0")).join(":"); } // Already full form, just normalize padding return ip .split(":") .map((part) => part.padStart(4, "0")) .join(":"); } // Helper export function for deep object validation export function validateObjectDeep( obj: any, maxDepth: number, currentDepth: number ): { success: boolean; errors: ValidationError[]; warnings: string[] } { const result = { success: true, errors: [] as ValidationError[], warnings: [] as string[], }; if (currentDepth > maxDepth) { result.success = false; result.errors.push( ErrorHandler.createError( [], `Maximum object depth of ${maxDepth} exceeded`, ErrorCode.SECURITY_VIOLATION, `object with depth <= ${maxDepth}`, currentDepth ) ); return result; } for (const [key, value] of Object.entries(obj)) { if (typeof value === "object" && value !== null && !Array.isArray(value)) { const deepResult = validateObjectDeep(value, maxDepth, currentDepth + 1); result.success = result.success && deepResult.success; result.errors.push(...deepResult.errors); result.warnings.push(...deepResult.warnings); } } return result; } // Helper export function for object schema validation export function validateObjectSchema( obj: any, schema: any ): { success: boolean; errors: ValidationError[]; warnings: string[] } { const result = { success: true, errors: [] as ValidationError[], warnings: [] as string[], }; if (schema.type && schema.type !== "object") { result.success = false; result.errors.push( ErrorHandler.createError( [], `Expected object type in schema`, ErrorCode.TYPE_MISMATCH, "object", schema.type ) ); return result; } if (schema.properties) { for (const [key, propSchema] of Object.entries(schema.properties)) { if (key in obj) { const propResult = validateJsonSchema(obj[key], propSchema); result.success = result.success && propResult.success; result.errors.push(...propResult.errors); result.warnings.push(...propResult.warnings); } } } return result; } // Helper interface for IP validation results export interface IPValidationResult { valid: boolean; normalized?: string; }