UNPKG

@buildappolis/env-manager

Version:

Enterprise-grade environment variable management system with encryption, snapshots, and validation

519 lines (512 loc) 18.6 kB
import { g as getDatabase } from '../../../chunks/session_DKC9ty2U.mjs'; import crypto from 'crypto'; import { G as GitUtils, g as getGitUtils } from '../../../chunks/git-utils_B6WJYd3b.mjs'; import fs from 'fs'; import path from 'path'; export { renderers } from '../../../renderers.mjs'; class ProjectValidator { constructor(database) { this.projectConfig = null; this.db = database; } async loadProjectConfig(config) { this.projectConfig = config; } async validateProjectRequirements(branch) { if (!this.projectConfig) { throw new Error("Project configuration not loaded"); } const results = { projectName: this.projectConfig.projectName, isValid: true, groups: {}, missing: [], invalid: [], canStart: false }; const currentVariables = this.db.getAllVariables(branch); const variableMap = new Map(currentVariables.map((v) => [v.name, v])); for (const [groupName, group] of Object.entries(this.projectConfig.requirements)) { const groupResult = { name: groupName, required: group.required, configured: true, variables: [], missing: [], invalid: [] }; for (const varConfig of group.variables) { const currentVar = variableMap.get(varConfig.name); const varResult = { 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 }; 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 { 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); } 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); } groupResult.configured = groupResult.missing.length === 0 && groupResult.invalid.length === 0; if (group.required && !groupResult.configured) { results.isValid = false; } results.groups[groupName] = groupResult; } 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() { if (!this.projectConfig) { throw new Error("Project configuration not loaded"); } const instructions = []; const validationResults = await this.validateProjectRequirements(); 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 || ""; if (varConfig.generateSecret) { value = this.generateSecret(); instructions.push({ type: "success", message: `Generated secret for ${varConfig.name}`, action: "auto_generated" }); } 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; } generateSecret(length = 32) { return crypto.randomBytes(length).toString("hex"); } getSetupInstructions() { if (!this.projectConfig) { return [{ type: "error", message: "No project configuration loaded", action: "load_config" }]; } const instructions = []; instructions.push({ type: "info", message: `Setting up ${this.projectConfig.projectName}`, action: "setup_start" }); 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 = { 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; } } class ConfigMigrator { constructor() { this.migrations = [ { version: "1.0.0", description: "Initial config structure", migrate: (config) => config }, { version: "1.1.0", description: "Add branch-specific configuration", migrate: (config) => { if (!config.branches) { config.branches = { enabled: true, strategy: "inherit", // 'inherit' | 'isolate' | 'merge' main: "main", environments: { development: ["dev", "develop", "feature/*"], staging: ["staging", "release/*"], production: ["main", "master", "prod"] } }; } if (!config.version) { config.version = "1.1.0"; } return config; } }, { version: "1.2.0", description: "Add deployment configuration", migrate: (config) => { if (!config.deployment) { config.deployment = { autoExport: false, exportOnCommit: false, branchSpecific: true, environments: {} }; } return config; } } ]; } async migrateConfig(configPath) { let config; try { if (configPath.endsWith(".ts")) { const { createJiti } = await import('jiti'); const jiti = createJiti(import.meta.url, { interopDefault: true, cache: false, requireCache: false }); const configModule = await jiti.import(configPath); config = configModule.default || configModule; } else { const cacheBuster = `?t=${Date.now()}`; const configModule = await import( /* @vite-ignore */ configPath + cacheBuster ); config = configModule.default || configModule; } } catch (error) { console.error("Failed to load config:", error); throw error; } const currentVersion = config.version || "1.0.0"; const latestVersion = this.migrations[this.migrations.length - 1].version; if (this.compareVersions(currentVersion, latestVersion) >= 0) { return config; } let migratedConfig = { ...config }; let applied = false; for (const migration of this.migrations) { if (this.compareVersions(currentVersion, migration.version) < 0) { console.log(`Applying migration: ${migration.description}`); migratedConfig = migration.migrate(migratedConfig); applied = true; } } if (applied) { migratedConfig.version = latestVersion; const projectDir = path.dirname(configPath); const backupDir = path.join(projectDir, ".env-manager", "backups"); if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); const backupPath = path.join(backupDir, `env.config.v${currentVersion}.${timestamp}.ts`); const originalContent = fs.readFileSync(configPath, "utf-8"); fs.writeFileSync(backupPath, originalContent); this.saveConfig(configPath, migratedConfig); console.log(`Config migrated from v${currentVersion} to v${latestVersion}`); console.log(`Backup saved to: ${backupPath}`); } return migratedConfig; } compareVersions(v1, v2) { const parts1 = v1.split(".").map((n) => parseInt(n, 10)); const parts2 = v2.split(".").map((n) => parseInt(n, 10)); for (let i = 0; i < 3; i++) { if (parts1[i] > parts2[i]) return 1; if (parts1[i] < parts2[i]) return -1; } return 0; } saveConfig(configPath, config) { const originalContent = fs.readFileSync(configPath, "utf-8"); const versionRegex = /version:\s*['"]([^'"]+)['"]/; const updatedContent = originalContent.replace(versionRegex, `version: '${config.version}'`); const headerRegex = /\/\/ Auto-migrated env\.config\.ts - Version [^\n]*/; const finalContent = updatedContent.replace(headerRegex, `// Auto-migrated env.config.ts - Version ${config.version}`); fs.writeFileSync(configPath, finalContent); } } class BranchAwareConfigLoader { constructor(projectRoot) { this.gitUtils = new GitUtils(projectRoot); this.migrator = new ConfigMigrator(); } async loadConfig(configPath) { const config = await this.migrator.migrateConfig(configPath); const gitInfo = await this.gitUtils.getGitInfo(); const currentBranch = gitInfo.branch || "main"; const enhancedConfig = { ...config, currentBranch, gitInfo, // Method to get branch-specific requirements getBranchRequirements: (branch) => { const targetBranch = branch || currentBranch; if (!config.branches?.enabled) { return config.requirements; } if (config.branchRequirements && config.branchRequirements[targetBranch]) { const strategy = config.branches.strategy || "inherit"; switch (strategy) { case "isolate": return config.branchRequirements[targetBranch]; case "merge": return this.mergeRequirements( config.requirements, config.branchRequirements[targetBranch] ); case "inherit": default: return this.inheritRequirements( config.requirements, config.branchRequirements[targetBranch] ); } } return config.requirements; }, // Method to determine environment from branch getEnvironment: () => { if (!config.branches?.environments) { return "development"; } for (const [env, patterns] of Object.entries(config.branches.environments)) { const branchPatterns = patterns; for (const pattern of branchPatterns) { if (this.matchBranchPattern(currentBranch, pattern)) { return env; } } } return "development"; } }; return enhancedConfig; } matchBranchPattern(branch, pattern) { const regex = pattern.replace(/\*/g, ".*").replace(/\?/g, "."); return new RegExp(`^${regex}$`).test(branch); } mergeRequirements(base, branch) { const merged = { ...base }; for (const [key, value] of Object.entries(branch)) { if (merged[key] && typeof value === "object" && value !== null) { const typedValue = value; merged[key] = { ...merged[key], ...typedValue, variables: [ ...merged[key].variables || [], ...typedValue.variables || [] ] }; } else { merged[key] = value; } } return merged; } inheritRequirements(base, branch) { const inherited = { ...base }; for (const [key, value] of Object.entries(branch)) { if (inherited[key] && typeof value === "object" && value !== null) { const typedValue = value; const baseVars = inherited[key].variables || []; const branchVars = typedValue.variables || []; const varMap = /* @__PURE__ */ new Map(); baseVars.forEach((v) => varMap.set(v.name, v)); branchVars.forEach((v) => varMap.set(v.name, { ...varMap.get(v.name), ...v })); inherited[key] = { ...inherited[key], ...value, variables: Array.from(varMap.values()) }; } else { inherited[key] = value; } } return inherited; } } function getBranchAwareConfigLoader(projectRoot) { return new BranchAwareConfigLoader(projectRoot); } const GET = async ({ url }) => { try { const projectPath = url.searchParams.get("projectPath"); const database = getDatabase(projectPath || void 0); const projectRoot = projectPath || process.env.PROJECT_ROOT || path.resolve(process.cwd(), ".."); const configPath = path.join(projectRoot, "env.config.ts"); const gitUtils = getGitUtils(projectRoot); const gitInfo = await gitUtils.getGitInfo(); const branches = await gitUtils.getAllBranches(); try { const configLoader = getBranchAwareConfigLoader(projectRoot); const projectConfig = await configLoader.loadConfig(configPath); const requirements = projectConfig.getBranchRequirements(); const environment = projectConfig.getEnvironment(); const validatorConfig = { projectName: projectConfig.projectName, version: projectConfig.projectVersion || "1.0.0", requirements: {}, validation: { requiredGroups: projectConfig.validation?.requiredGroups || [] } }; for (const [groupName, group] of Object.entries(requirements)) { const groupData = group; validatorConfig.requirements[groupName] = { name: groupName.charAt(0).toUpperCase() + groupName.slice(1), required: groupData.required, variables: groupData.variables.map((variable) => ({ name: variable.name, description: variable.description, required: true, sensitive: variable.sensitive, category: groupName, validation: variable.validation, minLength: variable.minLength, defaultValue: variable.default })) }; } let validationResults; try { const validator = new ProjectValidator(database); await validator.loadProjectConfig(validatorConfig); validationResults = await validator.validateProjectRequirements(gitInfo.branch); } catch (authError) { console.error("ProjectValidator error:", authError.message); validationResults = { projectName: validatorConfig.projectName, isValid: false, groups: {}, missing: [], invalid: [], canStart: false }; for (const [groupName, group] of Object.entries(validatorConfig.requirements)) { const groupData = group; validationResults.groups[groupName] = { name: groupData.name, required: groupData.required, configured: false, variables: groupData.variables.map((v) => ({ name: v.name, configured: false, hasValue: false, valid: false, errors: [], sensitive: v.sensitive, type: v.name.startsWith("NEXT_PUBLIC_") ? "client" : "server", description: v.description })), missing: groupData.variables.map((v) => v.name), invalid: [] }; validationResults.missing.push(...groupData.variables.map((v) => v.name)); } } const results = { ...validationResults, git: gitInfo, branches, currentBranch: gitInfo.branch || "main", environment, projectInfo: { name: projectConfig.projectName, version: projectConfig.projectVersion || "1.0.0", configVersion: projectConfig.version || "1.0.0", branchStrategy: projectConfig.branches?.strategy || "inherit" } }; return new Response(JSON.stringify(results), { status: 200, headers: { "Content-Type": "application/json" } }); } catch (configError) { console.error("Config loading error:", configError); return new Response(JSON.stringify({ error: "Failed to load project configuration", message: `Error loading env.config.ts: ${configError instanceof Error ? configError.message : String(configError)}` }), { status: 500, headers: { "Content-Type": "application/json" } }); } } catch (error) { console.error("Get project status error:", error); return new Response(JSON.stringify({ error: "Failed to get project status" }), { status: 500, headers: { "Content-Type": "application/json" } }); } }; const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({ __proto__: null, GET }, Symbol.toStringTag, { value: 'Module' })); const page = () => _page; export { page };