| import { PROVIDER_SUPPORTED_REQUESTS } from "../constants/config"; |
| import { BaseProvider, KnownProvider } from "../types/config"; |
|
|
| export interface ValidationRule { |
| isValid: boolean; |
| message: string; |
| } |
|
|
| export interface ValidationConfig { |
| rules: ValidationRule[]; |
| showAlways?: boolean; |
| } |
|
|
| export interface FieldValidation { |
| isValid: boolean; |
| message: string; |
| showTooltip: boolean; |
| } |
|
|
| export const validateField = (value: any, config: ValidationConfig, touched: boolean): FieldValidation => { |
| const invalidRule = config.rules.find((rule) => !rule.isValid); |
|
|
| return { |
| isValid: !invalidRule, |
| message: invalidRule?.message || "", |
| showTooltip: config.showAlways || (touched && !!invalidRule), |
| }; |
| }; |
|
|
| export interface ValidationResult { |
| isValid: boolean; |
| errors: string[]; |
| } |
|
|
| export const validateForm = (rules: ValidationRule[]): ValidationResult => { |
| const invalidRules = rules.filter((rule) => !rule.isValid); |
| return { |
| isValid: invalidRules.length === 0, |
| errors: invalidRules.map((rule) => rule.message), |
| }; |
| }; |
|
|
| export class Validator { |
| private rules: ValidationRule[]; |
|
|
| constructor(rules: ValidationRule[]) { |
| this.rules = rules.filter((rule) => rule !== undefined); |
| } |
|
|
| isValid(): boolean { |
| return !this.rules.some((rule) => !rule.isValid); |
| } |
|
|
| getErrors(): string[] { |
| return this.rules.filter((rule) => !rule.isValid).map((rule) => rule.message); |
| } |
|
|
| getFirstError(): string | undefined { |
| const firstInvalidRule = this.rules.find((rule) => !rule.isValid); |
| return firstInvalidRule?.message; |
| } |
|
|
| |
| static required(value: any, message = "This field is required"): ValidationRule { |
| return { |
| isValid: value !== undefined && value !== null && value !== "" && value !== 0, |
| message, |
| }; |
| } |
|
|
| static minValue(value: number, min: number, message = `Must be at least ${min}`): ValidationRule { |
| return { |
| isValid: !isNaN(value) && value >= min, |
| message, |
| }; |
| } |
|
|
| static maxValue(value: number, max: number, message = `Must be at most ${max}`): ValidationRule { |
| return { |
| isValid: !isNaN(value) && value <= max, |
| message, |
| }; |
| } |
|
|
| static pattern(value: string, regex: RegExp, message: string): ValidationRule { |
| return { |
| isValid: regex.test(value || ""), |
| message, |
| }; |
| } |
|
|
| static email(value: string, message = "Must be a valid email"): ValidationRule { |
| return this.pattern(value, /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, message); |
| } |
|
|
| static url(value: string, message = "Must be a valid URL"): ValidationRule { |
| return this.pattern(value, /^https?:\/\/.+/, message); |
| } |
|
|
| static minLength(value: string, min: number, message = `Must be at least ${min} characters`): ValidationRule { |
| return { |
| isValid: (value || "").length >= min, |
| message, |
| }; |
| } |
|
|
| static maxLength(value: string, max: number, message = `Must be at most ${max} characters`): ValidationRule { |
| return { |
| isValid: (value || "").length <= max, |
| message, |
| }; |
| } |
|
|
| static arrayMinLength<T>(array: T[], min: number, message = `Must have at least ${min} items`): ValidationRule { |
| return { |
| isValid: array?.length >= min, |
| message, |
| }; |
| } |
|
|
| static arrayMaxLength<T>(array: T[], max: number, message = `Must have at most ${max} items`): ValidationRule { |
| return { |
| isValid: array?.length <= max, |
| message, |
| }; |
| } |
|
|
| static arrayUnique<T>(array: T[], message = "Must have unique items"): ValidationRule { |
| return { |
| isValid: array?.length === new Set(array).size, |
| message, |
| }; |
| } |
|
|
| static arraysEqual<T>(array1: T[], array2: T[], message = "Must be equal"): ValidationRule { |
| return { |
| isValid: array1?.length === array2?.length && array1?.every((value, index) => value === array2[index]), |
| message, |
| }; |
| } |
|
|
| static custom(isValid: boolean, message: string): ValidationRule { |
| return { |
| isValid, |
| message, |
| }; |
| } |
|
|
| |
| static all(rules: ValidationRule[]): ValidationRule { |
| const invalidRule = rules.find((rule) => !rule.isValid); |
| return invalidRule || { isValid: true, message: "" }; |
| } |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| |
| export function isRedacted(value: string): boolean { |
| if (!value) { |
| return false; |
| } |
|
|
| |
| if (value.startsWith("env.")) { |
| return true; |
| } |
|
|
| |
| if (value.length === 32) { |
| const middle = value.substring(4, 28); |
| if (middle === "*".repeat(24)) { |
| return true; |
| } |
| } |
|
|
| |
| if (value.length <= 8 && /^\*+$/.test(value)) { |
| return true; |
| } |
|
|
| return false; |
| } |
|
|
| |
| |
| |
| |
| |
| export function isValidJSON(value: string): boolean { |
| try { |
| JSON.parse(value); |
| return true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function isValidVertexAuthCredentials(value: string): boolean { |
| if (!value || !value.trim()) { |
| return false; |
| } |
|
|
| |
| if (isRedacted(value)) { |
| return true; |
| } |
|
|
| |
| if (value.startsWith("env.")) { |
| return value.length > 4; |
| } |
|
|
| |
| try { |
| const parsed = JSON.parse(value); |
| return typeof parsed === "object" && parsed !== null && parsed.type === "service_account" && parsed.project_id && parsed.private_key; |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export function isValidDeployments(value: Record<string, string> | string | undefined): boolean { |
| if (!value) { |
| return false; |
| } |
|
|
| |
| if (typeof value === "object") { |
| return Object.keys(value).length > 0; |
| } |
|
|
| |
| if (typeof value === "string") { |
| |
| if (isRedacted(value)) { |
| return true; |
| } |
|
|
| |
| try { |
| const parsed = JSON.parse(value); |
| return typeof parsed === "object" && parsed !== null && Object.keys(parsed).length > 0; |
| } catch { |
| return false; |
| } |
| } |
|
|
| return false; |
| } |
|
|
| |
| |
| |
| |
| |
| export function isValidOrigin(origin: string): boolean { |
| if (!origin || !origin.trim()) { |
| return false; |
| } |
|
|
| |
| if (origin.trim() === "*") { |
| return true; |
| } |
|
|
| |
| if (origin.includes("*")) { |
| return isValidWildcardOrigin(origin); |
| } |
|
|
| try { |
| const url = new URL(origin); |
|
|
| |
| if (!url.protocol || !url.hostname) { |
| return false; |
| } |
|
|
| |
| if (!["http:", "https:"].includes(url.protocol)) { |
| return false; |
| } |
|
|
| |
| if (url.pathname !== "/" || url.search || url.hash) { |
| return false; |
| } |
|
|
| return true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function isValidWildcardOrigin(origin: string): boolean { |
| |
| if (!origin.startsWith("http://") && !origin.startsWith("https://")) { |
| return false; |
| } |
|
|
| |
| const protocolEnd = origin.indexOf("://") + 3; |
| const hostPart = origin.substring(protocolEnd); |
|
|
| |
| if (hostPart.includes("/") || hostPart.includes("?") || hostPart.includes("#")) { |
| return false; |
| } |
|
|
| |
| let hostname = hostPart; |
| if (hostPart.includes(":")) { |
| const parts = hostPart.split(":"); |
| if (parts.length !== 2) return false; |
| hostname = parts[0]; |
| const port = parts[1]; |
| |
| if (!/^\d+$/.test(port) || parseInt(port) < 1 || parseInt(port) > 65535) { |
| return false; |
| } |
| } |
|
|
| |
| |
| if (hostname === "*") { |
| return true; |
| } |
|
|
| |
| if (hostname.startsWith("*.")) { |
| const domain = hostname.substring(2); |
| |
| if (!domain || domain.includes("*") || domain.startsWith(".") || domain.endsWith(".")) { |
| return false; |
| } |
| |
| return /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(domain); |
| } |
|
|
| |
| if (hostname.includes("*")) { |
| return false; |
| } |
|
|
| return false; |
| } |
|
|
| |
| |
| |
| |
| |
| export function validateOrigins(origins: string[]): { isValid: boolean; invalidOrigins: string[] } { |
| if (!origins || origins.length === 0) { |
| return { isValid: true, invalidOrigins: [] }; |
| } |
|
|
| const invalidOrigins = origins.filter((origin) => !isValidOrigin(origin)); |
|
|
| return { |
| isValid: invalidOrigins.length === 0, |
| invalidOrigins, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function isValidRedisAddress(addr: string): boolean { |
| if (!addr) { |
| return false; |
| } |
|
|
| |
| const trimmedAddr = addr.trim(); |
| if (!trimmedAddr) { |
| return false; |
| } |
|
|
| try { |
| |
| if (trimmedAddr.startsWith("redis://") || trimmedAddr.startsWith("rediss://")) { |
| try { |
| const url = new URL(trimmedAddr); |
| const host = url.hostname; |
| const port = url.port || "6379"; |
|
|
| |
| const isIPv6Host = host.includes(":") || host.startsWith("["); |
| const hostToValidate = isIPv6Host ? host.replace(/^\[|\]$/g, "") : host; |
|
|
| const isValidHostResult = isIPv6Host ? isValidIPv6(hostToValidate) : isValidHost(hostToValidate); |
| return isValidHostResult && isValidPort(port); |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| const ipv6Match = trimmedAddr.match(/^\[([^\]]+)\]:(\d+)$/); |
| if (ipv6Match) { |
| const [, host, port] = ipv6Match; |
| return isValidIPv6(host) && isValidPort(port); |
| } |
|
|
| |
| const colonIndex = trimmedAddr.lastIndexOf(":"); |
| if (colonIndex === -1) { |
| return false; |
| } |
|
|
| const host = trimmedAddr.substring(0, colonIndex); |
| const port = trimmedAddr.substring(colonIndex + 1); |
|
|
| |
| return isValidHost(host) && isValidPort(port); |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function isValidHost(host: string): boolean { |
| if (!host || !host.trim()) { |
| return false; |
| } |
|
|
| const trimmedHost = host.trim(); |
|
|
| |
| if (trimmedHost.includes(":") || trimmedHost.startsWith("[")) { |
| |
| const ipv6Host = trimmedHost.replace(/^\[|\]$/g, ""); |
| return isValidIPv6(ipv6Host); |
| } |
|
|
| |
| |
| const hostPattern = /^[a-zA-Z0-9._-]+$/; |
| return hostPattern.test(trimmedHost) && trimmedHost.length <= 253; |
| } |
|
|
| |
| |
| |
| |
| |
| function isValidPort(port: string): boolean { |
| if (!port) { |
| return false; |
| } |
|
|
| const trimmedPort = port.trim(); |
|
|
| |
| if (!/^\d+$/.test(trimmedPort)) { |
| return false; |
| } |
|
|
| |
| const portNum = Number(trimmedPort); |
| return portNum >= 1 && portNum <= 65535; |
| } |
|
|
| |
| |
| |
| |
| |
| function isValidIPv6(host: string): boolean { |
| if (!host || !host.trim()) { |
| return false; |
| } |
|
|
| const trimmedHost = host.trim(); |
|
|
| |
| |
| const ipv6Pattern = |
| /^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$|^::$|^::1$|^([0-9a-fA-F]{0,4}:){0,6}::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$/; |
|
|
| |
| if (!ipv6Pattern.test(trimmedHost)) { |
| |
| const ipv6WithIpv4Pattern = /^([0-9a-fA-F]{0,4}:){1,6}(\d{1,3}\.){3}\d{1,3}$|^::([0-9a-fA-F]{0,4}:){0,5}(\d{1,3}\.){3}\d{1,3}$/; |
| if (!ipv6WithIpv4Pattern.test(trimmedHost)) { |
| return false; |
| } |
| } |
|
|
| |
| const parts = trimmedHost.split(":"); |
|
|
| |
| if (parts.length > 8) { |
| return false; |
| } |
|
|
| |
| for (const part of parts) { |
| if (part !== "" && !/^[0-9a-fA-F]{1,4}$/.test(part)) { |
| |
| if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(part)) { |
| return false; |
| } |
| } |
| } |
|
|
| return true; |
| } |
|
|
| export const isJson = (text: string) => { |
| try { |
| JSON.parse(text); |
| return true; |
| } catch { |
| return false; |
| } |
| }; |
|
|
| export const cleanJson = (text: unknown) => { |
| try { |
| if (typeof text === "string") return JSON.parse(text); |
| if (Array.isArray(text)) return text; |
| if (text !== null && typeof text === "object") return text; |
| if (typeof text === "number" || typeof text === "boolean") return text; |
| return "Invalid payload"; |
| } catch { |
| return text; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| export function isRequestTypeDisabled(providerType: BaseProvider | undefined, requestType: string): boolean { |
| if (!providerType) return false; |
|
|
| const supportedRequests = PROVIDER_SUPPORTED_REQUESTS[providerType]; |
| if (!supportedRequests) return false; |
|
|
| return !supportedRequests.includes(requestType); |
| } |
|
|
| |
| |
| |
| |
| |
| export function cleanPathOverrides(overrides?: Record<string, string | undefined>) { |
| if (!overrides) return undefined; |
|
|
| const entries = Object.entries(overrides) |
| .map(([k, v]) => [k, v?.trim()]) |
| .filter(([, v]) => v && v !== ""); |
|
|
| return entries.length ? (Object.fromEntries(entries) as Record<string, string>) : undefined; |
| } |
|
|