UNPKG

schema-env

Version:

Type-safe environment variable validation for Node.js using Zod schemas or custom adapters. Load .env files, expand variables, fetch async secrets, and validate process.env at startup.

275 lines 8.7 kB
// src/index.ts import fs from "node:fs"; import dotenv from "dotenv"; import { expand } from "dotenv-expand"; import { ZodError, ZodObject } from "zod"; var ZodValidatorAdapter = class { constructor(schema) { this.schema = schema; } validate(data) { const result = this.schema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } else { return { success: false, error: { // Map Zod errors to standardized format issues: result.error.errors.map((zodError) => ({ path: zodError.path, message: zodError.message })) } }; } } }; function _loadDotEnvFiles(dotEnvPath, nodeEnv) { if (dotEnvPath === false) { return {}; } let mergedDotEnvParsed = {}; const loadEnvFile = (filePath) => { try { const fileContent = fs.readFileSync(filePath, { encoding: "utf8" }); const parsed = dotenv.parse(fileContent); return parsed; } catch (e) { const err = e; if (err.code !== "ENOENT") { throw new Error( `\u274C Failed to load environment file from ${filePath}: ${err.message}` ); } return {}; } }; let pathsToLoad = []; if (dotEnvPath === void 0) { pathsToLoad = ["./.env"]; } else if (typeof dotEnvPath === "string") { pathsToLoad = [dotEnvPath]; } else if (Array.isArray(dotEnvPath)) { pathsToLoad = dotEnvPath.filter((path) => { if (typeof path !== "string") { console.warn( `\u26A0\uFE0F [schema-env] Warning: Invalid path ignored in dotEnvPath array: ${String( path )}` ); return false; } return true; }); } for (const path of pathsToLoad) { const parsed = loadEnvFile(path); mergedDotEnvParsed = { ...mergedDotEnvParsed, ...parsed }; } if (nodeEnv) { const envSpecificPath = `./.env.${nodeEnv}`; const envSpecificParsed = loadEnvFile(envSpecificPath); mergedDotEnvParsed = { ...mergedDotEnvParsed, ...envSpecificParsed }; } return mergedDotEnvParsed; } function _expandDotEnvValues(mergedDotEnvParsed, expandVariables, expandDotenv) { if (!expandVariables || !mergedDotEnvParsed || Object.keys(mergedDotEnvParsed).length === 0) { return mergedDotEnvParsed || {}; } const configToExpand = { parsed: { ...mergedDotEnvParsed } }; try { const expansionResult = expandDotenv(configToExpand); return expansionResult?.parsed || mergedDotEnvParsed || {}; } catch (e) { console.error( `\u274C Error during variable expansion: ${e instanceof Error ? e.message : String(e)}` ); return mergedDotEnvParsed || {}; } } function _mergeProcessEnv(sourceInput) { const sourceWithProcessEnv = { ...sourceInput }; for (const key in process.env) { if (Object.prototype.hasOwnProperty.call(process.env, key)) { const value = process.env[key]; if (value !== void 0) { sourceWithProcessEnv[key] = value; } } } return sourceWithProcessEnv; } function _formatValidationError(error) { let issues; if (error instanceof ZodError) { issues = error.errors.map((err) => ({ path: err.path, message: err.message })); } else if (error && Array.isArray(error.issues)) { issues = error.issues; } else { return "\u274C Unknown validation error occurred."; } const formattedErrors = issues.map( (err) => ` - ${err.path.join(".") || "UNKNOWN_PATH"}: ${err.message}` ); return `\u274C Invalid environment variables: ${formattedErrors.join("\n")}`; } function _validateEnvironment(adapter, sourceForValidation) { return adapter.validate(sourceForValidation); } async function _fetchSecrets(secretsSources) { if (!secretsSources || secretsSources.length === 0) { return {}; } const results = await Promise.allSettled( secretsSources.map((sourceFn, index) => { try { const maybePromise = sourceFn(); if (maybePromise && typeof maybePromise.then === "function") { return maybePromise; } else { return Promise.reject( new Error( `Sync return value from secrets source function at index ${index}. Function must return a Promise.` ) ); } } catch (syncError) { return Promise.reject( new Error( `Sync error in secrets source function at index ${index}: ${syncError instanceof Error ? syncError.message : String(syncError)}` ) ); } }) ); let mergedSecrets = {}; let successfulFetches = 0; results.forEach((result, index) => { if (result.status === "fulfilled") { successfulFetches++; if (result.value && typeof result.value === "object") { mergedSecrets = { ...mergedSecrets, ...result.value }; } else if (result.value !== void 0 && result.value !== null) { console.warn( `\u26A0\uFE0F [schema-env] Warning: Secrets source function at index ${index} resolved with non-object value: ${typeof result.value}. Expected Record<string, string | undefined>.` ); } } else { console.warn( `\u26A0\uFE0F [schema-env] Warning: Secrets source function at index ${index} failed: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}` ); } }); if (successfulFetches === 0 && secretsSources.length > 0) { console.warn( `\u26A0\uFE0F [schema-env] Warning: All ${secretsSources.length} provided secretsSources functions failed to resolve successfully.` ); return {}; } return mergedSecrets; } function _getValidatorAdapter(options) { const { schema, validator } = options; if (schema && validator) { throw new Error("Cannot provide both 'schema' and 'validator' options."); } if (validator) { return validator; } if (schema) { if (!(schema instanceof ZodObject)) { throw new Error( "Invalid 'schema' provided. Expected a ZodObject when 'validator' is not used." ); } return new ZodValidatorAdapter( schema ); } throw new Error("Must provide either a 'schema' or a 'validator' option."); } function createEnv(options) { const { dotEnvPath, expandVariables = false, _internalDotenvExpand = expand // _internalDotenvConfig removed } = options; const adapter = _getValidatorAdapter(options); const mergedDotEnvParsed = _loadDotEnvFiles( dotEnvPath, process.env.NODE_ENV // Use actual process.env value here for deciding which env-specific file to load // _internalDotenvConfig removed from call ); const finalDotEnvValues = _expandDotEnvValues( mergedDotEnvParsed, expandVariables, _internalDotenvExpand ); const sourceForValidation = _mergeProcessEnv(finalDotEnvValues); const validationResult = _validateEnvironment(adapter, sourceForValidation); if (!validationResult.success) { const errorMessage = _formatValidationError(validationResult.error); console.error(errorMessage); throw new Error("Environment validation failed. Check console output."); } return validationResult.data; } async function createEnvAsync(options) { const { dotEnvPath, expandVariables = false, secretsSources, _internalDotenvExpand = expand // _internalDotenvConfig removed } = options; const adapter = _getValidatorAdapter(options); const mergedDotEnvParsed = _loadDotEnvFiles( dotEnvPath, process.env.NODE_ENV // Use actual process.env value here for deciding which env-specific file to load // _internalDotenvConfig removed from call ); const expandedDotEnvValues = _expandDotEnvValues( mergedDotEnvParsed, expandVariables, _internalDotenvExpand ); try { const secretsValues = await _fetchSecrets(secretsSources); const sourceBeforeProcessEnv = { ...expandedDotEnvValues, ...secretsValues }; const sourceForValidation = _mergeProcessEnv(sourceBeforeProcessEnv); const validationResult = _validateEnvironment(adapter, sourceForValidation); if (!validationResult.success) { const errorMessage = _formatValidationError(validationResult.error); console.error(errorMessage); throw new Error("Environment validation failed. Check console output."); } return validationResult.data; } catch (error) { if (error instanceof Error) { return Promise.reject(error); } else { return Promise.reject( new Error(`An unexpected error occurred: ${String(error)}`) ); } } } export { createEnv, createEnvAsync }; //# sourceMappingURL=index.js.map