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
text/typescript
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;
}