dotenv-gad 0.1.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kasim Lyee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # dotenv-gad
2
2
 
3
- [![npm version](https://badge.fury.io/js/dotenv-gad.svg)](https://badge.fury.io/js/dotenv-guard)
3
+ [![npm version](https://badge.fury.io/js/dotenv-gad.svg)](https://badge.fury.io/js/dotenv-gad)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
6
  **dotenv-gad** is an environment variable validation tool that brings type safety and schema validation to your Node.js and JavaScript applications. It extends `dotenv` with features like:
7
7
 
8
8
  - Type-safe environment variables
9
9
  - Schema validation
10
+ - Schema composition
10
11
  - Automatic documentation generation
11
12
  - TypeScript support
12
13
  - CLI tooling
@@ -61,6 +62,8 @@ console.log(`Server running on port ${env.PORT}`);
61
62
  | `sync` | Generate/update .env.example |
62
63
  | `types` | Generate env.d.ts TypeScript types |
63
64
  | `init` | Create starter schema |
65
+ | `fix` | Fixes environment issues |
66
+ | `docs` | Generates .env documentation |
64
67
 
65
68
  ```bash
66
69
  npx dotenv-gad check
@@ -195,8 +198,8 @@ Environment validation failed:
195
198
  }
196
199
  ```
197
200
 
198
- ## 📜 License
201
+ ## License
199
202
 
200
203
  MIT © [Kasim Lyee]
201
204
 
202
- Contributions are welcome!!
205
+ [![contribution](https://github.com/kasimlyee/dotenv-gad/blob/main/CONTRIBUTING.md)] are welcome!!
package/dist/cli/index.js CHANGED
@@ -9,6 +9,8 @@ import checkCommand from "./commands/check.js";
9
9
  import syncCommand from "./commands/sync.js";
10
10
  import typesCommand from "./commands/types.js";
11
11
  import initCommand from "./commands/init.js";
12
+ import fixCommand from "./commands/fix.js";
13
+ import docsCommand from "./commands/docs.js";
12
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
15
  const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8"));
14
16
  export function createCLI() {
@@ -22,7 +24,14 @@ export function createCLI() {
22
24
  .option("--debug", "Enable debug output")
23
25
  .option("--env <file>", "Specify env file path", ".env")
24
26
  .option("--schema <file>", "Specify schema file path", "env.schema.ts");
25
- const commands = [checkCommand, syncCommand, typesCommand, initCommand];
27
+ const commands = [
28
+ checkCommand,
29
+ syncCommand,
30
+ typesCommand,
31
+ initCommand,
32
+ fixCommand,
33
+ docsCommand,
34
+ ];
26
35
  commands.forEach((command) => {
27
36
  const cmd = command(program);
28
37
  program.addCommand(cmd);
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { EnvValidator } from "./validator.js";
2
2
  import { defineSchema } from "./schema.js";
3
- import { AggregateError } from "./errors.js";
3
+ import { AggregateError, EnvValidationError } from "./errors.js";
4
4
  import { loadEnv, createEnvProxy } from "./utils.js";
5
+ import { composeSchema } from "./compose.js";
6
+ import loadSchema from "./cli/commands/types.js";
7
+ import { applyFix } from "./cli/commands/utils.js";
5
8
  import dotenv from "dotenv";
6
- export { defineSchema, AggregateError, EnvValidator, loadEnv, createEnvProxy };
9
+ export { defineSchema, AggregateError, EnvValidationError, EnvValidator, loadEnv, createEnvProxy, composeSchema, loadSchema, applyFix, };
7
10
  export function validateEnv(schema, options) {
8
11
  const env = dotenv.config().parsed || {};
9
12
  const validator = new EnvValidator(schema, options);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export default function (program: Command): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export default function (program: Command): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export default function (program: Command): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export default function (program: Command): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export default function (program: Command): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export default function (program: Command): Command;
@@ -0,0 +1,20 @@
1
+ import { SchemaDefinition } from "../../schema.js";
2
+ /**
3
+ * Loads a schema from a file.
4
+ * @param schemaPath Path to the schema file.
5
+ * @returns The loaded schema.
6
+ * @throws If the schema file is malformed or cannot be loaded.
7
+ */
8
+ export declare function loadSchema(schemaPath: string): Promise<SchemaDefinition>;
9
+ /**
10
+ * Applies fixes to the given environment file by prompting the user to input
11
+ * values for missing variables and variables that do not match the schema.
12
+ *
13
+ * @param issues An object where each key is the name of an environment variable
14
+ * and the value is an object containing the `value` property (which is the
15
+ * invalid value) and the `key` property (which is the name of the variable).
16
+ * @param schema The schema definition for the environment variables.
17
+ * @param envPath The path to the environment file. Defaults to `.env`.
18
+ * @throws If the schema is malformed or if the user cancels the prompt.
19
+ */
20
+ export declare function applyFix(issues: Record<string, any>, schema: SchemaDefinition, envPath?: string): Promise<void>;
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ export declare function createCLI(): Command;
4
+ declare const program: Command;
5
+ export default program;
@@ -0,0 +1,22 @@
1
+ import { SchemaDefinition } from "./schema.js";
2
+ /**
3
+ * Compose multiple schema definitions into a single one.
4
+ *
5
+ * @example
6
+ * const dbSchema = {
7
+ * DB_HOST: { type: "string" },
8
+ * DB_USER: { type: "string" },
9
+ * DB_PASSWORD: { type: "string", sensitive: true },
10
+ * };
11
+ *
12
+ * const appSchema = {
13
+ * APP_NAME: { type: "string" },
14
+ * APP_PORT: { type: "number" },
15
+ * };
16
+ *
17
+ * const fullSchema = composeSchema(dbSchema, appSchema);
18
+ *
19
+ * @param {...SchemaDefinition[]} schemas - schema definitions to compose
20
+ * @returns {SchemaDefinition} - a single schema definition with all the properties
21
+ */
22
+ export declare function composeSchema(...schemas: SchemaDefinition[]): SchemaDefinition;
@@ -0,0 +1,22 @@
1
+ import { SchemaRule } from "./schema.js";
2
+ export declare class EnvValidationError extends Error {
3
+ key: string;
4
+ message: string;
5
+ receiveValue?: unknown | undefined;
6
+ constructor(key: string, message: string, receiveValue?: unknown | undefined);
7
+ }
8
+ export declare class AggregateError extends Error {
9
+ errors: {
10
+ key: string;
11
+ message: string;
12
+ value?: any;
13
+ rule?: SchemaRule;
14
+ }[];
15
+ constructor(errors: {
16
+ key: string;
17
+ message: string;
18
+ value?: any;
19
+ rule?: SchemaRule;
20
+ }[], message: string);
21
+ toString(): string;
22
+ }
@@ -0,0 +1,12 @@
1
+ import { EnvValidator } from "./validator.js";
2
+ import { defineSchema, SchemaDefinition, SchemaRule } from "./schema.js";
3
+ import { AggregateError, EnvValidationError } from "./errors.js";
4
+ import { loadEnv, createEnvProxy } from "./utils.js";
5
+ import { composeSchema } from "./compose.js";
6
+ import loadSchema from "./cli/commands/types.js";
7
+ import { applyFix } from "./cli/commands/utils.js";
8
+ export { defineSchema, AggregateError, EnvValidationError, EnvValidator, loadEnv, createEnvProxy, composeSchema, loadSchema, applyFix, };
9
+ export type { SchemaDefinition, SchemaRule };
10
+ export declare function validateEnv(schema: SchemaDefinition, options?: {
11
+ strict?: boolean;
12
+ }): Record<string, any>;
@@ -0,0 +1,35 @@
1
+ type PrimitiveType = "string" | "number" | "boolean" | "date";
2
+ type ComplexType = "object" | "array" | "email" | "url" | "ip" | "json" | "port";
3
+ type SchemaType = PrimitiveType | ComplexType;
4
+ export interface SchemaRule {
5
+ type: SchemaType;
6
+ required?: boolean;
7
+ default?: any;
8
+ min?: number;
9
+ max?: number;
10
+ minLength?: number;
11
+ maxLength?: number;
12
+ validate?: (value: any) => boolean;
13
+ transform?: (value: any) => any;
14
+ sensitive?: boolean;
15
+ docs?: string;
16
+ enum?: any[];
17
+ regex?: RegExp;
18
+ regexError?: string;
19
+ error?: string;
20
+ items?: SchemaRule;
21
+ properties?: Record<string, SchemaRule>;
22
+ env?: {
23
+ [envName: string]: Partial<SchemaRule>;
24
+ };
25
+ }
26
+ export type SchemaDefinition = Record<string, SchemaRule>;
27
+ /**
28
+ * Define a schema for a set of environment variables.
29
+ *
30
+ * @param schema A record where each key is the name of an environment variable
31
+ * and the value is a `SchemaRule` object that defines the rules for that
32
+ * variable.
33
+ */
34
+ export declare function defineSchema(schema: SchemaDefinition): SchemaDefinition;
35
+ export {};
@@ -1,5 +1,4 @@
1
- import dotenv from "dotenv";
2
- import { EnvValidator } from "./validator.js";
1
+ import { SchemaDefinition } from "./schema.js";
3
2
  /**
4
3
  * Load the environment variables from a .env file, validate them against the schema
5
4
  * and return an object with the validated values.
@@ -9,11 +8,9 @@ import { EnvValidator } from "./validator.js";
9
8
  *
10
9
  * @returns A validated object with the environment variables.
11
10
  */
12
- export function loadEnv(schema, options) {
13
- const env = dotenv.config().parsed || {};
14
- const validator = new EnvValidator(schema, options);
15
- return validator.validate(env);
16
- }
11
+ export declare function loadEnv(schema: SchemaDefinition, options?: {
12
+ strict?: boolean;
13
+ }): Record<string, any>;
17
14
  /**
18
15
  * Create a proxy around the validated environment variables. The proxy will
19
16
  * throw an error if you try to access a variable that is not validated.
@@ -23,14 +20,4 @@ export function loadEnv(schema, options) {
23
20
  * @returns A proxy object that throws an error if you access an
24
21
  * unvalidated variable.
25
22
  */
26
- export function createEnvProxy(validatedEnv) {
27
- return new Proxy(validatedEnv, {
28
- get(target, prop) {
29
- const value = target[prop];
30
- if (value === undefined) {
31
- throw new Error(`Environment variable ${String(prop)} is not validated`);
32
- }
33
- return value;
34
- },
35
- });
36
- }
23
+ export declare function createEnvProxy<T extends Record<string, any>>(validatedEnv: T): T;
@@ -0,0 +1,12 @@
1
+ import { SchemaDefinition } from "./schema.js";
2
+ export declare class EnvValidator {
3
+ private schema;
4
+ private options?;
5
+ private errors;
6
+ constructor(schema: SchemaDefinition, options?: {
7
+ strict?: boolean;
8
+ } | undefined);
9
+ validate(env: Record<string, string | undefined>): Record<string, any>;
10
+ private validateKey;
11
+ private getEffectiveRule;
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotenv-gad",
3
- "version": "0.1.0",
3
+ "version": "1.0.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -13,12 +13,6 @@
13
13
  "dev": "tsc --watch",
14
14
  "prepublishOnly": "npm run build && npm test"
15
15
  },
16
- "jest": {
17
- "extensionsToTreatAsEsm": [
18
- ".ts"
19
- ],
20
- "transform": {}
21
- },
22
16
  "keywords": [
23
17
  "dotenv",
24
18
  "environment",
@@ -35,7 +29,7 @@
35
29
  "./package.json": "./package.json"
36
30
  },
37
31
  "files": [
38
- "dist/**/*",
32
+ "dist/**/*.d.ts",
39
33
  "README.md",
40
34
  "LICENSE"
41
35
  ],
@@ -63,6 +57,7 @@
63
57
  "ts-jest": "^29.4.0"
64
58
  },
65
59
  "dependencies": {
60
+ "dotenv": "^16.5.0",
66
61
  "esbuild": "^0.25.5"
67
62
  }
68
63
  }
@@ -1,41 +0,0 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import ora from "ora";
4
- import { validateEnv } from "../../index.js";
5
- import { AggregateError } from "../../errors.js";
6
- import { loadSchema } from "./utils.js";
7
- export default function (program) {
8
- return new Command("check")
9
- .description("Validate .env against schema")
10
- .option("--strict", "Fail on extra env vars not in schema")
11
- .option("--fix", "Attempt to fix errors interactively")
12
- .action(async (option, command) => {
13
- const rootOpts = command.parent.opts();
14
- const spinner = ora("Validatng environment.......").start();
15
- try {
16
- const schema = await loadSchema(rootOpts.schema);
17
- const env = validateEnv(schema, {
18
- strict: option.strict,
19
- });
20
- spinner.succeed(chalk.green("Environment validation passed!"));
21
- console.log(chalk.dim(`Found ${Object.keys(env).length} valid variables`));
22
- }
23
- catch (error) {
24
- spinner.stop();
25
- if (error instanceof AggregateError) {
26
- console.error(chalk.red("\nEnvironment validation failed:"));
27
- error.errors.forEach((e) => {
28
- console.log(` ${chalk.yellow(e.key)}: ${e.message}`);
29
- if (e.rule?.docs) {
30
- console.log(chalk.dim(` ${e.rule.docs}`));
31
- }
32
- });
33
- process.exit(1);
34
- }
35
- else {
36
- console.error(chalk.red("Unexpected error:"), error);
37
- process.exit(2);
38
- }
39
- }
40
- });
41
- }
@@ -1,61 +0,0 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import ora from "ora";
4
- import { writeFileSync, existsSync } from "fs";
5
- import inquirer from "inquirer";
6
- import { dirname } from "path";
7
- import { fileURLToPath } from "url";
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- export default function (program) {
10
- return new Command("init")
11
- .description("Initialize new schema file")
12
- .option("--force", "Overwrite existing files")
13
- .action(async (options, command) => {
14
- const rootOpts = command.parent.opts();
15
- const schemaPath = rootOpts.schema;
16
- if (existsSync(schemaPath)) {
17
- if (!options.force) {
18
- const { overwrite } = await inquirer.prompt({
19
- type: "confirm",
20
- name: "overwrite",
21
- message: "Schema file already exists. Overwrite?",
22
- default: false,
23
- });
24
- if (!overwrite)
25
- process.exit(0);
26
- }
27
- }
28
- const spinner = ora("Creating new schema...").start();
29
- try {
30
- const template = `import { defineSchema } from 'dotenv-gad';
31
-
32
- export default defineSchema({
33
- // Add your environment variables here
34
- PORT: {
35
- type: 'number',
36
- default: 3000,
37
- docs: 'Port to run the server on'
38
- },
39
- NODE_ENV: {
40
- type: 'string',
41
- enum: ['development', 'production', 'test'],
42
- default: 'development'
43
- },
44
- DB_URL: {
45
- type: 'string',
46
- required: true,
47
- docs: 'Database connection URL'
48
- }
49
- });
50
- `;
51
- writeFileSync(schemaPath, template);
52
- spinner.succeed(chalk.green(`Created ${schemaPath} successfully!`));
53
- console.log(chalk.dim("\nEdit this file to define your environment schema"));
54
- }
55
- catch (error) {
56
- spinner.fail(chalk.red("Failed to create schema file"));
57
- console.error(error);
58
- process.exit(1);
59
- }
60
- });
61
- }
@@ -1,37 +0,0 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import ora from "ora";
4
- import { writeFileSync } from "fs";
5
- import { loadSchema } from "./utils.js";
6
- export default function (program) {
7
- return new Command("sync")
8
- .description("Genearte/update .env.example file")
9
- .option("--output <file>", "Output file path", ".env.example")
10
- .action(async (options, command) => {
11
- const rootOpts = command.parent.opts();
12
- const spinner = ora("Generating .env.example.......").start();
13
- try {
14
- const schema = await loadSchema(rootOpts.schema);
15
- let exampleContent = "# Auto-generated by dotenv-gad\n\n";
16
- Object.entries(schema).forEach(([key, rule]) => {
17
- if (rule.sensitive)
18
- return;
19
- exampleContent += `# ${rule.docs || "No description available"}\n`;
20
- exampleContent += `# Type: ${rule.type}\n`;
21
- if (rule.default !== undefined) {
22
- exampleContent += `# Default: ${JSON.stringify(rule.default)}\n`;
23
- }
24
- exampleContent += `${key}=${rule.default
25
- ? JSON.stringify(rule.default)
26
- : ""}\n\n`;
27
- });
28
- writeFileSync(options.output, exampleContent.trim());
29
- spinner.succeed(chalk.green(`Generated ${options.output} successfully!`));
30
- }
31
- catch (error) {
32
- spinner.fail(chalk.red(`Failed to generate .env.example`));
33
- console.error(error);
34
- process.exit(1);
35
- }
36
- });
37
- }
@@ -1,40 +0,0 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import ora from "ora";
4
- import { writeFileSync } from "fs";
5
- import { loadSchema } from "./utils.js";
6
- export default function (program) {
7
- return new Command("types")
8
- .description("Generate Typescript types")
9
- .option("--output <file>", "Output file path", "env.d.ts")
10
- .action(async (options, command) => {
11
- const rootOpts = command.parent.opts();
12
- const spinner = ora("Generating type definitions.......").start();
13
- try {
14
- const schema = await loadSchema(rootOpts.schema);
15
- let typeContent = "// Auto-generated by dotenv-gad\n\ndeclare namespace NodeJS{\n interface ProcessEnv{\n";
16
- Object.entries(schema).forEach(([key, rule]) => {
17
- let type;
18
- switch (rule.type) {
19
- case "number":
20
- type = "number";
21
- break;
22
- case "boolean":
23
- type = "boolean";
24
- break;
25
- default:
26
- type = "string";
27
- }
28
- typeContent += ` ${key}${rule.required ? "" : "?"}:${type};\n`;
29
- });
30
- typeContent += " }\n}\n";
31
- writeFileSync(options.output, typeContent);
32
- spinner.succeed(chalk.green(`Generated ${options.output} successfully!`));
33
- }
34
- catch (error) {
35
- spinner.fail(chalk.red("Failed to generate type definitions"));
36
- console.error(error);
37
- process.exit(1);
38
- }
39
- });
40
- }
@@ -1,40 +0,0 @@
1
- import { readFileSync, writeFileSync, unlinkSync } from "fs";
2
- import { dirname, join } from "path";
3
- import { fileURLToPath } from "url";
4
- import { transformSync } from "esbuild";
5
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
- export async function loadSchema(schemaPath) {
7
- try {
8
- if (schemaPath.endsWith(".ts")) {
9
- const code = readFileSync(schemaPath, "utf-8");
10
- const result = transformSync(code, {
11
- format: "esm",
12
- loader: "ts",
13
- target: "esnext",
14
- });
15
- // Create temporary file
16
- const tempFile = join(__dirname, "../../temp-schema.mjs");
17
- writeFileSync(tempFile, result.code);
18
- try {
19
- // Import with query string to bust cache
20
- const module = await import(`${tempFile}?t=${Date.now()}`);
21
- return module.default;
22
- }
23
- finally {
24
- // Clean up temp file
25
- unlinkSync(tempFile);
26
- }
27
- }
28
- else if (schemaPath.endsWith(".js")) {
29
- const module = await import(schemaPath);
30
- return module.default;
31
- }
32
- else if (schemaPath.endsWith(".json")) {
33
- return JSON.parse(readFileSync(schemaPath, "utf-8"));
34
- }
35
- throw new Error("Unsupported schema format. Use .ts, .js or .json");
36
- }
37
- catch (error) {
38
- throw new Error(`Failed to load schema: ${error.message}`);
39
- }
40
- }
package/dist/errors.js DELETED
@@ -1,22 +0,0 @@
1
- export class AggregateError extends Error {
2
- errors;
3
- constructor(errors, message) {
4
- super(message);
5
- this.errors = errors;
6
- this.name = "AggregateError";
7
- Object.setPrototypeOf(this, AggregateError.prototype);
8
- }
9
- toString() {
10
- const errorList = this.errors
11
- .map((e) => {
12
- let msg = ` - ${e.key}:${e.message}`;
13
- if (e.value !== undefined)
14
- msg += ` (reaceived: ${JSON.stringify(e.value)})`;
15
- if (e.rule?.docs)
16
- msg += `\n (${e.rule.docs})`;
17
- return msg;
18
- })
19
- .join("\n");
20
- return `${this.message}:\n${errorList}`;
21
- }
22
- }
package/dist/schema.js DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * Define a schema for a set of environment variables.
3
- *
4
- * @param schema A record where each key is the name of an environment variable
5
- * and the value is a `SchemaRule` object that defines the rules for that
6
- * variable.
7
- */
8
- export function defineSchema(schema) {
9
- return schema;
10
- }
package/dist/validator.js DELETED
@@ -1,180 +0,0 @@
1
- import { AggregateError } from "./errors.js";
2
- export class EnvValidator {
3
- schema;
4
- options;
5
- errors = [];
6
- constructor(schema, options) {
7
- this.schema = schema;
8
- this.options = options;
9
- }
10
- validate(env) {
11
- this.errors = [];
12
- const result = {};
13
- for (const [key, rule] of Object.entries(this.schema)) {
14
- try {
15
- result[key] = this.validateKey(key, rule, env[key]);
16
- }
17
- catch (error) {
18
- if (error instanceof Error) {
19
- this.errors.push({
20
- key,
21
- message: error.message,
22
- value: env[key],
23
- rule,
24
- });
25
- }
26
- }
27
- }
28
- if (this.errors.length > 0) {
29
- const keys = this.errors.map((e) => e.key).join(", ");
30
- throw new AggregateError(this.errors, `Environment validation failed: ${keys}`);
31
- }
32
- if (this.options?.strict) {
33
- const extraVars = Object.keys(env).filter((k) => !(k in this.schema));
34
- if (extraVars.length > 0) {
35
- throw new Error(`Unexpected environment variables: ${extraVars.join(", ")}`);
36
- }
37
- }
38
- return result;
39
- }
40
- validateKey(key, rule, value) {
41
- const effectiveRule = this.getEffectiveRule(key, rule);
42
- if (value === undefined) {
43
- if (effectiveRule.required)
44
- throw new Error(`Missing required environment variable`);
45
- return effectiveRule.default;
46
- }
47
- if (effectiveRule.transform) {
48
- value = effectiveRule.transform(value);
49
- }
50
- switch (effectiveRule.type) {
51
- case "string":
52
- value = String(value).trim();
53
- if (effectiveRule.minLength && value.length < effectiveRule.minLength) {
54
- throw new Error(`Environment variable ${key} must be at least ${effectiveRule.minLength} characters`);
55
- }
56
- if (effectiveRule.maxLength && value.length > effectiveRule.maxLength) {
57
- throw new Error(`Environment variable ${key} must be at most ${effectiveRule.maxLength} characters`);
58
- }
59
- break;
60
- case "number":
61
- value = Number(value);
62
- if (isNaN(value)) {
63
- throw new Error(`Environment variable ${key} must be a number`);
64
- }
65
- if (effectiveRule.min !== undefined && value < effectiveRule.min) {
66
- throw new Error(`Environment variable ${key} must be at least ${effectiveRule.min}`);
67
- }
68
- if (effectiveRule.max !== undefined && value > effectiveRule.max) {
69
- throw new Error(`Environment variable ${key} must be at most ${effectiveRule.max}`);
70
- }
71
- break;
72
- case "boolean":
73
- if (typeof value === "string") {
74
- value = value.toLowerCase();
75
- if (value === "true") {
76
- value = true;
77
- }
78
- else if (value === "false") {
79
- value = false;
80
- }
81
- }
82
- if (typeof value !== "boolean") {
83
- throw new Error(`Environment variable ${key} must be a boolean (true/false)`);
84
- }
85
- value = Boolean(value);
86
- break;
87
- case "url":
88
- try {
89
- new URL(String(value));
90
- }
91
- catch {
92
- throw new Error("Must be a valid URL");
93
- }
94
- break;
95
- case "email":
96
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
97
- throw new Error("Must be a valid email");
98
- }
99
- break;
100
- case "ip":
101
- if (!/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(String(value))) {
102
- throw new Error("Must be a valid IP address");
103
- }
104
- break;
105
- case "port":
106
- const port = Number(value);
107
- if (isNaN(port))
108
- throw new Error("Must be a number");
109
- if (port < 1 || port > 65535) {
110
- throw new Error("Must be between 1 and 65535");
111
- }
112
- break;
113
- case "json":
114
- try {
115
- value = JSON.parse(value);
116
- }
117
- catch {
118
- throw new Error("Must be valid JSON");
119
- }
120
- break;
121
- case "array":
122
- if (!Array.isArray(value)) {
123
- try {
124
- value = JSON.parse(value);
125
- }
126
- catch {
127
- throw new Error("Must be a valid array or JSON array string");
128
- }
129
- }
130
- if (effectiveRule.items) {
131
- value = value.map((item, i) => {
132
- try {
133
- return this.validateKey(`${key}[${i}]`, effectiveRule.items, item);
134
- }
135
- catch (error) {
136
- throw new Error(`Array item '${i}':${error.message}`);
137
- }
138
- });
139
- }
140
- break;
141
- case "object":
142
- if (typeof value === "string") {
143
- try {
144
- value = JSON.parse(value);
145
- }
146
- catch {
147
- throw new Error("Must be a valid object or JSON string");
148
- }
149
- }
150
- if (effectiveRule.properties) {
151
- const obj = {};
152
- for (const [prop, propRule] of Object.entries(effectiveRule.properties)) {
153
- try {
154
- obj[prop] = this.validateKey(`${key}.${prop}`, propRule, value[prop]);
155
- }
156
- catch (error) {
157
- throw new Error(`Property '${prop}':${error.message}`);
158
- }
159
- }
160
- value = obj;
161
- }
162
- break;
163
- }
164
- if (effectiveRule.enum && !effectiveRule.enum.includes(value)) {
165
- throw new Error(`Environment variable ${key} must be one of ${effectiveRule.enum.join(", ")}`);
166
- }
167
- if (effectiveRule.regex && !effectiveRule.regex.test(value)) {
168
- throw new Error(`Environment variable ${key} must match ${effectiveRule.regex}`);
169
- }
170
- if (effectiveRule.validate && !effectiveRule.validate(value)) {
171
- throw new Error(effectiveRule.error || "Custom validation failed");
172
- }
173
- return value;
174
- }
175
- getEffectiveRule(key, rule) {
176
- const envName = process.env.NODE_ENV || "development";
177
- const envRule = rule.env?.[envName] || {};
178
- return { ...rule, ...envRule };
179
- }
180
- }