From ea09db6ba45c86d5024292cec3d5200fccd2612d Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sat, 31 Jan 2026 20:27:36 +0000 Subject: [PATCH] Initial upload with CI/CD workflow --- src/core/validator.ts | 190 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/core/validator.ts diff --git a/src/core/validator.ts b/src/core/validator.ts new file mode 100644 index 0000000..9c9f956 --- /dev/null +++ b/src/core/validator.ts @@ -0,0 +1,190 @@ +import { cleanEnv, str, num, bool, email, url, port, json, host, makeValidator } from 'envalid'; +import { EnvSchema, EnvType, ValidationResult, ValidationError } from './types'; + +const enumValidator = makeValidator((input: string) => { + return input; +}); + +export function createValidator(schema: EnvSchema) { + const validators: Record> = {}; + + for (const [key, config] of Object.entries(schema.variables)) { + switch (config.type) { + case 'string': + validators[key] = str({ default: config.default as string | undefined, desc: config.desc }); + break; + case 'number': + case 'int': + validators[key] = num({ default: config.default as number | undefined, desc: config.desc }); + break; + case 'boolean': + validators[key] = bool({ default: config.default as boolean | undefined, desc: config.desc }); + break; + case 'email': + validators[key] = email({ default: config.default as string | undefined, desc: config.desc }); + break; + case 'url': + validators[key] = url({ default: config.default as string | undefined, desc: config.desc }); + break; + case 'port': + validators[key] = port({ default: config.default as number | undefined, desc: config.desc }); + break; + case 'json': + validators[key] = json({ default: config.default as string | undefined, desc: config.desc }); + break; + case 'enum': + validators[key] = enumValidator({ + choices: config.choices || [], + default: config.default as string | undefined, + desc: config.desc + }); + break; + case 'host': + validators[key] = host({ default: config.default as string | undefined, desc: config.desc }); + break; + default: + validators[key] = str({ default: config.default as string | undefined, desc: config.desc }); + } + } + + return cleanEnv(process.env, validators); +} + +export function validateEnv(envData: Record, schema: EnvSchema): ValidationResult { + const errors: ValidationError[] = []; + + for (const [key, config] of Object.entries(schema.variables)) { + const value = envData[key]; + + if (config.required !== false && (value === undefined || value === '')) { + errors.push({ + field: key, + message: `Required variable "${key}" is missing`, + value + }); + continue; + } + + if (value !== undefined && value !== '') { + try { + validateType(key, value, config.type, config.choices); + } catch (e) { + errors.push({ + field: key, + message: (e as Error).message, + value + }); + } + } + } + + return { + valid: errors.length === 0, + errors, + output: {} + }; +} + +function validateType(key: string, value: string, type: EnvType, choices?: string[]): void { + switch (type) { + case 'number': + case 'int': + if (isNaN(Number(value))) { + throw new Error(`"${key}" must be a valid number, got: ${value}`); + } + break; + case 'boolean': + if (value !== 'true' && value !== 'false' && value !== '1' && value !== '0') { + throw new Error(`"${key}" must be a boolean (true/false), got: ${value}`); + } + break; + case 'port': { + const port = Number(value); + if (isNaN(port) || port < 1 || port > 65535) { + throw new Error(`"${key}" must be a valid port number (1-65535), got: ${value}`); + } + break; + } + case 'email': { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(value)) { + throw new Error(`"${key}" must be a valid email, got: ${value}`); + } + break; + } + case 'url': + try { + new URL(value); + } catch { + throw new Error(`"${key}" must be a valid URL, got: ${value}`); + } + break; + case 'json': + try { + JSON.parse(value); + } catch { + throw new Error(`"${key}" must be valid JSON, got: ${value}`); + } + break; + case 'enum': + if (choices && !choices.includes(value)) { + throw new Error(`"${key}" must be one of: ${choices.join(', ')}, got: ${value}`); + } + break; + } +} + +export function parseSchemaFromEnv(envData: Record): EnvSchema { + const variables: Record = {}; + + for (const [key, value] of Object.entries(envData)) { + const inferred = inferType(key, value); + variables[key] = { + type: inferred.type, + required: true, + desc: `Environment variable ${key}` + }; + } + + return { variables }; +} + +function inferType(key: string, value: string): { type: EnvType; default?: string | number | boolean } { + if (value === '') { + return { type: 'string', default: '' }; + } + + if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false' || value === '1' || value === '0') { + return { type: 'boolean' }; + } + + if (!isNaN(Number(value)) && value.trim() !== '') { + return { type: Number.isInteger(Number(value)) ? 'int' : 'number' }; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (emailRegex.test(value)) { + return { type: 'email' }; + } + + try { + new URL(value); + return { type: 'url' }; + } catch { + // Not a URL + } + + const port = Number(value); + if (!isNaN(port) && port >= 1 && port <= 65535) { + return { type: 'port' }; + } + + try { + JSON.parse(value); + return { type: 'json' }; + } catch { + // Not JSON + } + + return { type: 'string' }; +}