@buildappolis/env-manager
Version:
Enterprise-grade environment variable management system with encryption, snapshots, and validation
220 lines (186 loc) • 7.05 kB
text/typescript
import type {
ProjectConfig,
ValidationResults,
GroupResult,
VariableResult,
SetupInstruction
} from '../types.js'
import type EnvDatabase from './database.js'
import crypto from 'crypto'
export default class ProjectValidator {
private db: EnvDatabase
private projectConfig: ProjectConfig | null = null
constructor(database: EnvDatabase) {
this.db = database
}
async loadProjectConfig(config: ProjectConfig): Promise<void> {
this.projectConfig = config
}
async validateProjectRequirements(branch?: string): Promise<ValidationResults> {
if (!this.projectConfig) {
throw new Error('Project configuration not loaded')
}
const results: ValidationResults = {
projectName: this.projectConfig.projectName,
isValid: true,
groups: {},
missing: [],
invalid: [],
canStart: false
}
// Get current variables from database with the specified branch
const currentVariables = this.db.getAllVariables(branch)
const variableMap = new Map(currentVariables.map(v => [v.name, v]))
// Validate each requirement group
for (const [groupName, group] of Object.entries(this.projectConfig.requirements)) {
const groupResult: GroupResult = {
name: groupName,
required: group.required,
configured: true,
variables: [],
missing: [],
invalid: []
}
// Check each variable in the group
for (const varConfig of group.variables) {
const currentVar = variableMap.get(varConfig.name)
const varResult: VariableResult = {
name: varConfig.name,
configured: !!currentVar,
hasValue: !!(currentVar && currentVar.value),
valid: true,
errors: [],
sensitive: varConfig.sensitive,
type: varConfig.name.startsWith('NEXT_PUBLIC_') ? 'client' : 'server',
description: varConfig.description
}
// Check if variable exists and has value
if (!currentVar) {
varResult.configured = false
varResult.valid = false
varResult.errors.push('Variable not configured')
groupResult.missing.push(varConfig.name)
results.missing.push(varConfig.name)
} else if (!currentVar.value) {
varResult.hasValue = false
varResult.valid = false
varResult.errors.push('Variable has no value')
groupResult.missing.push(varConfig.name)
results.missing.push(varConfig.name)
} else {
// Validate value format if validation rules exist
if (varConfig.validation && typeof varConfig.validation.test === 'function' && !varConfig.validation.test(currentVar.value)) {
varResult.valid = false
varResult.errors.push('Value format invalid')
groupResult.invalid.push(varConfig.name)
results.invalid.push(varConfig.name)
}
// Check minimum length
if (varConfig.minLength && currentVar.value.length < varConfig.minLength) {
varResult.valid = false
varResult.errors.push(`Value too short (min: ${varConfig.minLength})`)
groupResult.invalid.push(varConfig.name)
results.invalid.push(varConfig.name)
}
}
groupResult.variables.push(varResult)
}
// Group is configured if all variables are valid
groupResult.configured = groupResult.missing.length === 0 && groupResult.invalid.length === 0
// If group is required and not configured, project is invalid
if (group.required && !groupResult.configured) {
results.isValid = false
}
results.groups[groupName] = groupResult
}
// Check if required groups are satisfied
const requiredGroups = this.projectConfig.validation?.requiredGroups || []
const satisfiedRequiredGroups = requiredGroups.filter(groupName =>
results.groups[groupName]?.configured
)
results.canStart = results.isValid && satisfiedRequiredGroups.length === requiredGroups.length
return results
}
async autoConfigureProject(): Promise<SetupInstruction[]> {
if (!this.projectConfig) {
throw new Error('Project configuration not loaded')
}
const instructions: SetupInstruction[] = []
const validationResults = await this.validateProjectRequirements()
// Auto-generate missing variables
for (const [groupName, group] of Object.entries(this.projectConfig.requirements)) {
for (const varConfig of group.variables) {
if (validationResults.missing.includes(varConfig.name)) {
let value = varConfig.defaultValue || ''
// Generate secret if needed
if (varConfig.generateSecret) {
value = this.generateSecret()
instructions.push({
type: 'success',
message: `Generated secret for ${varConfig.name}`,
action: 'auto_generated'
})
}
// Set the variable
if (value) {
this.db.setVariable(varConfig.name, value, {
category: varConfig.category || 'other',
description: varConfig.description || '',
sensitive: varConfig.sensitive || false
})
instructions.push({
type: 'success',
message: `Configured ${varConfig.name}`,
action: 'auto_configured'
})
} else {
instructions.push({
type: 'warning',
message: `${varConfig.name} needs manual configuration`,
action: 'manual_required'
})
}
}
}
}
return instructions
}
private generateSecret(length: number = 32): string {
return crypto.randomBytes(length).toString('hex')
}
getSetupInstructions(): SetupInstruction[] {
if (!this.projectConfig) {
return [{
type: 'error',
message: 'No project configuration loaded',
action: 'load_config'
}]
}
const instructions: SetupInstruction[] = []
instructions.push({
type: 'info',
message: `Setting up ${this.projectConfig.projectName}`,
action: 'setup_start'
})
// Add instructions for each requirement group
for (const [groupName, group] of Object.entries(this.projectConfig.requirements)) {
instructions.push({
type: 'info',
message: `Configure ${group.name} (${group.variables.length} variables)`,
action: 'configure_group'
})
for (const varConfig of group.variables) {
const instruction: SetupInstruction = {
type: varConfig.required ? 'warning' : 'info',
message: `Set ${varConfig.name}: ${varConfig.description || 'No description'}`,
action: 'set_variable'
}
if (varConfig.generateSecret) {
instruction.message += ' (will auto-generate secret)'
}
instructions.push(instruction)
}
}
return instructions
}
}