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.
208 lines (205 loc) • 9.19 kB
JavaScript
import { ErrorHandler } from '../schema/mode/interfaces/errors/ErrorHandler.js';
import { ErrorCode } from '../schema/mode/interfaces/errors/types/errors.type.js';
// Helper export function for deep JSON validation
function validateJsonDeep(data, options) {
const result = {
success: true,
errors: [],
warnings: [],
};
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
function validateJsonSchema(data, schema) {
// Basic schema validation - you might want to use a proper JSON schema library like Ajv
const result = {
success: true,
errors: [],
warnings: [],
};
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
function validateIPv4(ip, strict) {
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
function validateIPv6(ip, strict) {
// 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
function normalizeIPv6(ip) {
// 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
function validateObjectDeep(obj, maxDepth, currentDepth) {
const result = {
success: true,
errors: [],
warnings: [],
};
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
function validateObjectSchema(obj, schema) {
const result = {
success: true,
errors: [],
warnings: [],
};
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;
}
export { normalizeIPv6, validateIPv4, validateIPv6, validateJsonDeep, validateJsonSchema, validateObjectDeep, validateObjectSchema };
//# sourceMappingURL=securityHelpers.js.map