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.

311 lines (310 loc) 10.5 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { createEnv: () => createEnv, createEnvAsync: () => createEnvAsync }); module.exports = __toCommonJS(index_exports); var import_node_fs = __toESM(require("fs"), 1); var import_dotenv = __toESM(require("dotenv"), 1); var import_dotenv_expand = require("dotenv-expand"); var import_zod = require("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 = import_node_fs.default.readFileSync(filePath, { encoding: "utf8" }); const parsed = import_dotenv.default.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 import_zod.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 import_zod.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 = import_dotenv_expand.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 = import_dotenv_expand.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)}`) ); } } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createEnv, createEnvAsync }); //# sourceMappingURL=index.cjs.map