dotenv-gad 1.4.2 → 1.5.0
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/README.md +47 -10
- package/dist/cli/commands/check.js +10 -2
- package/dist/cli/commands/decrypt.js +126 -0
- package/dist/cli/commands/docs.js +2 -2
- package/dist/cli/commands/encrypt.js +94 -0
- package/dist/cli/commands/fix.js +16 -12
- package/dist/cli/commands/init.js +17 -12
- package/dist/cli/commands/keygen.js +97 -0
- package/dist/cli/commands/rotate.js +160 -0
- package/dist/cli/commands/status.js +116 -0
- package/dist/cli/commands/sync.js +15 -17
- package/dist/cli/commands/utils.js +34 -28
- package/dist/cli/commands/verify.js +101 -0
- package/dist/cli/index.js +12 -0
- package/dist/crypto.js +162 -0
- package/dist/errors.js +92 -4
- package/dist/index.cjs +375 -24
- package/dist/index.js +5 -4
- package/dist/types/cli/commands/check.d.ts +1 -1
- package/dist/types/cli/commands/decrypt.d.ts +2 -0
- package/dist/types/cli/commands/docs.d.ts +1 -1
- package/dist/types/cli/commands/encrypt.d.ts +2 -0
- package/dist/types/cli/commands/fix.d.ts +1 -1
- package/dist/types/cli/commands/init.d.ts +1 -1
- package/dist/types/cli/commands/keygen.d.ts +2 -0
- package/dist/types/cli/commands/rotate.d.ts +2 -0
- package/dist/types/cli/commands/status.d.ts +2 -0
- package/dist/types/cli/commands/sync.d.ts +1 -1
- package/dist/types/cli/commands/verify.d.ts +2 -0
- package/dist/types/crypto.d.ts +58 -0
- package/dist/types/errors.d.ts +35 -12
- package/dist/types/index.d.ts +7 -3
- package/dist/types/schema.d.ts +7 -0
- package/dist/types/utils.d.ts +19 -10
- package/dist/types/validator.d.ts +40 -0
- package/dist/utils.js +30 -12
- package/dist/validator.js +161 -18
- package/package.json +1 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface KeyPair {
|
|
2
|
+
/** Hex-encoded 44-byte SPKI DER. Store as ENVGAD_PUBLIC_KEY in .env (safe to commit). */
|
|
3
|
+
publicKeyHex: string;
|
|
4
|
+
/** Hex-encoded 48-byte PKCS8 DER. Store as ENVGAD_PRIVATE_KEY in .env.keys (never commit). */
|
|
5
|
+
privateKeyHex: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Generates a new key pair using the X25519 curve.
|
|
9
|
+
*
|
|
10
|
+
* @returns An object containing the hex-encoded public and private keys.
|
|
11
|
+
* The public key is safe to commit to version control (e.g. .env), while the private key
|
|
12
|
+
* should be kept secret and stored securely (e.g. .env.keys).
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateKeyPair(): KeyPair;
|
|
15
|
+
/**
|
|
16
|
+
* Encrypts a plaintext string using ChaCha20-Poly1305 authenticated encryption.
|
|
17
|
+
* The encryption key is derived from the shared secret of the ephemeral key pair
|
|
18
|
+
* and the recipient's public key using HKDF-SHA256.
|
|
19
|
+
*
|
|
20
|
+
* @param plaintext - The plaintext string to encrypt.
|
|
21
|
+
* @param recipientPublicKeyHex - Hex-encoded 44-byte SPKI DER of the recipient's public key.
|
|
22
|
+
* @param varName - The name of the environment variable being encrypted.
|
|
23
|
+
* @returns A wire-format string containing the encrypted ciphertext and
|
|
24
|
+
* ephemeral public key information: `encrypted:v1:<base64>`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function encryptEnvValue(plaintext: string, recipientPublicKeyHex: string, varName: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Decrypt a wire-format ciphertext produced by {@link encryptEnvValue}.
|
|
29
|
+
*
|
|
30
|
+
* @param token - Wire-format string: `encrypted:v1:<base64>`.
|
|
31
|
+
* @param privateKeyHex - Hex-encoded PKCS8 DER of the recipient's private key (from ENVGAD_PRIVATE_KEY).
|
|
32
|
+
* @param varName - Schema variable name, must match the one used during encryption (AAD check).
|
|
33
|
+
* @returns Decrypted plaintext string.
|
|
34
|
+
* @throws {@link DecryptionFailedError} if the key is wrong, ciphertext is tampered, or AAD mismatches.
|
|
35
|
+
*/
|
|
36
|
+
export declare function decryptEnvValue(token: string, privateKeyHex: string, varName: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Returns true if the given value starts with the encrypted value
|
|
39
|
+
* prefix, and false otherwise.
|
|
40
|
+
*
|
|
41
|
+
* The encrypted value prefix is in the format of
|
|
42
|
+
* `encrypted:v1:<base64-encoded-payload>`.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} value the value to check
|
|
45
|
+
* @returns {boolean} true if the value is an encrypted value, false otherwise
|
|
46
|
+
*/
|
|
47
|
+
export declare function isEncryptedValue(value: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Loads the ENVGAD_PRIVATE_KEY value from the given path (default: `.env.keys`)
|
|
50
|
+
* or the ENVGAD_PRIVATE_KEY environment variable if present.
|
|
51
|
+
* Returns the hex-encoded DER value of the private key if found, or null otherwise.
|
|
52
|
+
* @param {Object} [options] Optional options.
|
|
53
|
+
* @param {string} [options.keysPath] Path to the `.env.keys` file (default: `.env.keys`).
|
|
54
|
+
* @returns {string | null} Hex-encoded DER value of the private key if found, or null otherwise.
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadPrivateKey(options?: {
|
|
57
|
+
keysPath?: string;
|
|
58
|
+
}): string | null;
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -4,21 +4,44 @@ export declare class EnvValidationError extends Error {
|
|
|
4
4
|
message: string;
|
|
5
5
|
receiveValue?: unknown | undefined;
|
|
6
6
|
constructor(key: string, message: string, receiveValue?: unknown | undefined);
|
|
7
|
+
static [Symbol.hasInstance](instance: unknown): boolean;
|
|
7
8
|
}
|
|
9
|
+
type ErrorItem = {
|
|
10
|
+
key: string;
|
|
11
|
+
message: string;
|
|
12
|
+
value?: any;
|
|
13
|
+
rule?: SchemaRule;
|
|
14
|
+
};
|
|
8
15
|
export declare class EnvAggregateError extends Error {
|
|
9
|
-
errors:
|
|
10
|
-
|
|
11
|
-
|
|
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);
|
|
16
|
+
get errors(): ErrorItem[];
|
|
17
|
+
constructor(errors: ErrorItem[], message: string);
|
|
18
|
+
static [Symbol.hasInstance](instance: unknown): boolean;
|
|
21
19
|
toString(): string;
|
|
22
20
|
}
|
|
23
21
|
/** @deprecated Use `EnvAggregateError` instead — avoids shadowing the built-in `AggregateError`. */
|
|
24
22
|
export declare const AggregateError: typeof EnvAggregateError;
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when a required encryption or decryption key is not available.
|
|
25
|
+
* For encryption: ENVGAD_PUBLIC_KEY missing from .env.
|
|
26
|
+
* For decryption: ENVGAD_PRIVATE_KEY missing from .env.keys and environment.
|
|
27
|
+
*/
|
|
28
|
+
export declare class EncryptionKeyMissingError extends Error {
|
|
29
|
+
constructor(context: "encryption" | "decryption");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Thrown when ChaCha20-Poly1305 authenticated decryption fails.
|
|
33
|
+
* Common causes: wrong private key, corrupted ciphertext, or AAD mismatch
|
|
34
|
+
* (ciphertext copied from a different variable).
|
|
35
|
+
*/
|
|
36
|
+
export declare class DecryptionFailedError extends Error {
|
|
37
|
+
constructor(varName: string);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Thrown when an encrypted/plaintext value is found but the schema says otherwise.
|
|
41
|
+
* When `shouldBeEncrypted = true`: schema has `encrypted: true` but value is plaintext.
|
|
42
|
+
* When `shouldBeEncrypted = false`: value starts with `encrypted:v1:` but schema lacks `encrypted: true`.
|
|
43
|
+
*/
|
|
44
|
+
export declare class EncryptedFieldMismatchError extends Error {
|
|
45
|
+
constructor(varName: string, shouldBeEncrypted: boolean);
|
|
46
|
+
}
|
|
47
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { EnvValidator } from "./validator.js";
|
|
2
2
|
import { defineSchema, SchemaDefinition, SchemaRule } from "./schema.js";
|
|
3
|
-
import { EnvAggregateError, AggregateError, EnvValidationError } from "./errors.js";
|
|
3
|
+
import { EnvAggregateError, AggregateError, EnvValidationError, EncryptionKeyMissingError, DecryptionFailedError, EncryptedFieldMismatchError } from "./errors.js";
|
|
4
4
|
import { loadEnv, createEnvProxy } from "./utils.js";
|
|
5
5
|
import { composeSchema } from "./compose.js";
|
|
6
6
|
import { ExtractEnv, InferEnv } from "./types.js";
|
|
7
|
+
import { generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey } from "./crypto.js";
|
|
8
|
+
import type { KeyPair } from "./crypto.js";
|
|
7
9
|
export { defineSchema, EnvAggregateError,
|
|
8
10
|
/** @deprecated Use `EnvAggregateError` instead. */
|
|
9
|
-
AggregateError, EnvValidationError, EnvValidator, loadEnv, createEnvProxy, composeSchema, };
|
|
10
|
-
export type { SchemaDefinition, SchemaRule, ExtractEnv, InferEnv };
|
|
11
|
+
AggregateError, EnvValidationError, EncryptionKeyMissingError, DecryptionFailedError, EncryptedFieldMismatchError, EnvValidator, loadEnv, createEnvProxy, composeSchema, generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey, };
|
|
12
|
+
export type { SchemaDefinition, SchemaRule, ExtractEnv, InferEnv, KeyPair };
|
|
11
13
|
export declare function validateEnv(schema: SchemaDefinition, options?: {
|
|
12
14
|
strict?: boolean;
|
|
13
15
|
path?: string;
|
|
16
|
+
allowPlaintext?: boolean;
|
|
17
|
+
keysPath?: string;
|
|
14
18
|
}): Record<string, any>;
|
package/dist/types/schema.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export interface SchemaRule {
|
|
|
12
12
|
validate?: (value: any) => boolean;
|
|
13
13
|
transform?: (value: any) => any;
|
|
14
14
|
sensitive?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* When true, the value must be stored encrypted in the .env file using the
|
|
17
|
+
* dotenv-gad ECIES scheme. The validator will automatically decrypt the value
|
|
18
|
+
* before type-checking and returning it to the application.
|
|
19
|
+
* Use `npx dotenv-gad keygen` and `npx dotenv-gad encrypt` to manage keys.
|
|
20
|
+
*/
|
|
21
|
+
encrypted?: boolean;
|
|
15
22
|
docs?: string;
|
|
16
23
|
enum?: any[];
|
|
17
24
|
regex?: RegExp;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import { SchemaDefinition } from "./schema.js";
|
|
2
2
|
import type { InferEnv } from "./types.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
4
|
+
* Silently reads and parses a .env file without injecting into process.env.
|
|
5
|
+
* Returns an empty object when the file does not exist (e.g. Vercel, Railway,
|
|
6
|
+
* Docker — where env vars are already present in process.env).
|
|
7
|
+
*/
|
|
8
|
+
export declare function readEnvFile(path?: string): Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Loads environment variables from a `.env` file (if present) and validates them
|
|
11
|
+
* against the provided schema.
|
|
6
12
|
*
|
|
7
|
-
* @
|
|
8
|
-
* @param
|
|
9
|
-
* @param
|
|
10
|
-
* @param
|
|
11
|
-
* @param
|
|
12
|
-
* @param
|
|
13
|
-
* @param
|
|
14
|
-
* @
|
|
13
|
+
* @param schema The schema definition for the environment variables.
|
|
14
|
+
* @param options Optional options for the validation process.
|
|
15
|
+
* @param options.strict When true, environment variables not present in the schema will be rejected.
|
|
16
|
+
* @param options.includeRaw When true, include raw values in error reports (non-sensitive by default).
|
|
17
|
+
* @param options.includeSensitive When true, include values marked sensitive in error reports (use only for local debugging).
|
|
18
|
+
* @param options.path Path to the `.env` file (defaults to `.env` in cwd).
|
|
19
|
+
* @param options.allowPlaintext When true, fields with `encrypted: true` that have plaintext values emit a warning instead of an error.
|
|
20
|
+
* @param options.keysPath Path to the `.env.keys` file containing ENVGAD_PRIVATE_KEY (default: `.env.keys`).
|
|
21
|
+
* @returns The validated environment variables, typed according to the schema.
|
|
15
22
|
*/
|
|
16
23
|
export declare function loadEnv<S extends SchemaDefinition>(schema: S, options?: {
|
|
17
24
|
strict?: boolean;
|
|
18
25
|
includeRaw?: boolean;
|
|
19
26
|
includeSensitive?: boolean;
|
|
20
27
|
path?: string;
|
|
28
|
+
allowPlaintext?: boolean;
|
|
29
|
+
keysPath?: string;
|
|
21
30
|
}): InferEnv<S>;
|
|
22
31
|
/**
|
|
23
32
|
* Create a proxy around the validated environment variables. The proxy will
|
|
@@ -2,21 +2,61 @@ import { SchemaDefinition } from "./schema.js";
|
|
|
2
2
|
export declare class EnvValidator {
|
|
3
3
|
private schema;
|
|
4
4
|
private options?;
|
|
5
|
+
private static readonly EMAIL_REGEX;
|
|
6
|
+
private static readonly LOCAL_MAX;
|
|
7
|
+
private static readonly DOMAIN_MAX;
|
|
8
|
+
private static readonly DOMAIN_PART_MAX;
|
|
5
9
|
private errors;
|
|
6
10
|
/**
|
|
7
11
|
* Constructs a new EnvValidator instance.
|
|
8
12
|
* @param {SchemaDefinition} schema The schema definition for the environment variables.
|
|
9
13
|
* @param {Object} [options] Optional options for the validation process.
|
|
10
14
|
* @param {boolean} [options.strict] When true, environment variables not present in the schema will be rejected.
|
|
15
|
+
* @param {boolean} [options.allowPlaintext] When true, fields with `encrypted: true` that have plaintext values emit a warning instead of an error. Useful for gradual migration.
|
|
16
|
+
* @param {string} [options.keysPath] Path to the `.env.keys` file (default: `.env.keys`).
|
|
11
17
|
*/
|
|
12
18
|
constructor(schema: SchemaDefinition, options?: {
|
|
13
19
|
strict?: boolean;
|
|
14
20
|
includeRaw?: boolean;
|
|
15
21
|
includeSensitive?: boolean;
|
|
22
|
+
allowPlaintext?: boolean;
|
|
23
|
+
keysPath?: string;
|
|
16
24
|
} | undefined);
|
|
25
|
+
/** Bundler-safe instanceof — checks the symbol marker, not the class name. */
|
|
26
|
+
static [Symbol.hasInstance](instance: unknown): boolean;
|
|
17
27
|
validate(env: Record<string, string | undefined>): Record<string, any>;
|
|
28
|
+
/**
|
|
29
|
+
* Redacts a value to hide its contents, following these rules:
|
|
30
|
+
* - If `value` is undefined, returns undefined.
|
|
31
|
+
* - If `sensitive` is true, returns `"****"`.
|
|
32
|
+
* - If `value` is not a string, returns the original value.
|
|
33
|
+
* - If `value` is a string longer than 64 characters, truncates it to 4 characters
|
|
34
|
+
* at the start and end, and replaces the middle with `"..."`.
|
|
35
|
+
* - Otherwise, returns the original string.
|
|
36
|
+
* @param value The value to redact.
|
|
37
|
+
* @param sensitive If true, redacts the value to `"****"`.
|
|
38
|
+
*/
|
|
18
39
|
private redactValue;
|
|
40
|
+
/**
|
|
41
|
+
* Tries to parse the given value as a JSON object.
|
|
42
|
+
* Returns `{ ok: true, value: JSON.parse(s) }` if successful,
|
|
43
|
+
* or `{ ok: false }` if not.
|
|
44
|
+
* The following conditions will cause the function to return `{ ok: false }`:
|
|
45
|
+
* - `value` is not a string
|
|
46
|
+
* - `value` is an empty string
|
|
47
|
+
* - `value` does not start with one of the following characters: `{`, `[`, `"`, `t`, `f`, `n`, or a digit
|
|
48
|
+
* - `value` cannot be parsed as a JSON object
|
|
49
|
+
*/
|
|
19
50
|
private tryParseJSON;
|
|
20
51
|
private validateKey;
|
|
21
52
|
private getEffectiveRule;
|
|
53
|
+
/**
|
|
54
|
+
* Preprocesses environment variables to detect and handle encrypted values.
|
|
55
|
+
* 1. Decrypts any encrypted values using the private key.
|
|
56
|
+
* 2. Checks if any plaintext values are present for fields that should be encrypted.
|
|
57
|
+
* 3. Flags any env value that looks encrypted but the schema doesn't declare encrypted: true.
|
|
58
|
+
* @returns An object containing the processed environment variables and a set of keys that were skipped due to errors.
|
|
59
|
+
*/
|
|
60
|
+
private preprocessEncryption;
|
|
61
|
+
private validateEmail;
|
|
22
62
|
}
|
package/dist/utils.js
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parse as parseEnv } from "dotenv";
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
3
|
import { EnvValidator } from "./validator.js";
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
5
|
+
* Silently reads and parses a .env file without injecting into process.env.
|
|
6
|
+
* Returns an empty object when the file does not exist (e.g. Vercel, Railway,
|
|
7
|
+
* Docker — where env vars are already present in process.env).
|
|
8
|
+
*/
|
|
9
|
+
export function readEnvFile(path) {
|
|
10
|
+
const filePath = path ?? ".env";
|
|
11
|
+
if (!existsSync(filePath))
|
|
12
|
+
return {};
|
|
13
|
+
try {
|
|
14
|
+
return parseEnv(readFileSync(filePath, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Loads environment variables from a `.env` file (if present) and validates them
|
|
22
|
+
* against the provided schema.
|
|
6
23
|
*
|
|
7
|
-
* @
|
|
8
|
-
* @param
|
|
9
|
-
* @param
|
|
10
|
-
* @param
|
|
11
|
-
* @param
|
|
12
|
-
* @param
|
|
13
|
-
* @param
|
|
14
|
-
* @
|
|
24
|
+
* @param schema The schema definition for the environment variables.
|
|
25
|
+
* @param options Optional options for the validation process.
|
|
26
|
+
* @param options.strict When true, environment variables not present in the schema will be rejected.
|
|
27
|
+
* @param options.includeRaw When true, include raw values in error reports (non-sensitive by default).
|
|
28
|
+
* @param options.includeSensitive When true, include values marked sensitive in error reports (use only for local debugging).
|
|
29
|
+
* @param options.path Path to the `.env` file (defaults to `.env` in cwd).
|
|
30
|
+
* @param options.allowPlaintext When true, fields with `encrypted: true` that have plaintext values emit a warning instead of an error.
|
|
31
|
+
* @param options.keysPath Path to the `.env.keys` file containing ENVGAD_PRIVATE_KEY (default: `.env.keys`).
|
|
32
|
+
* @returns The validated environment variables, typed according to the schema.
|
|
15
33
|
*/
|
|
16
34
|
export function loadEnv(schema, options) {
|
|
17
|
-
const fileEnv =
|
|
35
|
+
const fileEnv = readEnvFile(options?.path);
|
|
18
36
|
const env = { ...process.env, ...fileEnv };
|
|
19
37
|
const validator = new EnvValidator(schema, options);
|
|
20
38
|
return validator.validate(env);
|
package/dist/validator.js
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
|
-
import { EnvAggregateError } from "./errors.js";
|
|
1
|
+
import { EnvAggregateError, EncryptionKeyMissingError } from "./errors.js";
|
|
2
|
+
import { decryptEnvValue, isEncryptedValue, loadPrivateKey } from "./crypto.js";
|
|
3
|
+
import net from "net";
|
|
4
|
+
const kValidator = Symbol.for("dotenv-gad.EnvValidator");
|
|
2
5
|
export class EnvValidator {
|
|
3
6
|
schema;
|
|
4
7
|
options;
|
|
8
|
+
static EMAIL_REGEX = /^[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
|
|
9
|
+
static LOCAL_MAX = 64;
|
|
10
|
+
static DOMAIN_MAX = 255;
|
|
11
|
+
static DOMAIN_PART_MAX = 63;
|
|
5
12
|
errors = [];
|
|
6
13
|
/**
|
|
7
14
|
* Constructs a new EnvValidator instance.
|
|
8
15
|
* @param {SchemaDefinition} schema The schema definition for the environment variables.
|
|
9
16
|
* @param {Object} [options] Optional options for the validation process.
|
|
10
17
|
* @param {boolean} [options.strict] When true, environment variables not present in the schema will be rejected.
|
|
18
|
+
* @param {boolean} [options.allowPlaintext] When true, fields with `encrypted: true` that have plaintext values emit a warning instead of an error. Useful for gradual migration.
|
|
19
|
+
* @param {string} [options.keysPath] Path to the `.env.keys` file (default: `.env.keys`).
|
|
11
20
|
*/
|
|
12
21
|
constructor(schema, options) {
|
|
13
22
|
this.schema = schema;
|
|
14
23
|
this.options = options;
|
|
24
|
+
Object.defineProperty(this, kValidator, { value: true, enumerable: false });
|
|
25
|
+
}
|
|
26
|
+
/** Bundler-safe instanceof — checks the symbol marker, not the class name. */
|
|
27
|
+
static [Symbol.hasInstance](instance) {
|
|
28
|
+
return (typeof instance === "object" &&
|
|
29
|
+
instance !== null &&
|
|
30
|
+
Object.prototype.hasOwnProperty.call(instance, kValidator));
|
|
15
31
|
}
|
|
16
32
|
validate(env) {
|
|
17
33
|
this.errors = [];
|
|
34
|
+
// First Decrypt encrypted fields and check for schema/value mismatches.
|
|
35
|
+
const { processedEnv, skipKeys } = this.preprocessEncryption(env);
|
|
18
36
|
const result = {};
|
|
19
37
|
// Build grouping map for object types that support envPrefix.
|
|
20
38
|
// We'll collect all prefixes first and then make a single pass over env keys
|
|
@@ -29,29 +47,31 @@ export class EnvValidator {
|
|
|
29
47
|
groupedEnv[k] = {};
|
|
30
48
|
}
|
|
31
49
|
}
|
|
32
|
-
const envKeys = Object.keys(
|
|
50
|
+
const envKeys = Object.keys(processedEnv);
|
|
33
51
|
for (let i = 0; i < envKeys.length; i++) {
|
|
34
52
|
const eKey = envKeys[i];
|
|
35
53
|
for (let j = 0; j < prefixes.length; j++) {
|
|
36
54
|
const { key, prefix } = prefixes[j];
|
|
37
55
|
if (eKey.startsWith(prefix)) {
|
|
38
56
|
const subKey = eKey.slice(prefix.length);
|
|
39
|
-
groupedEnv[key][subKey] =
|
|
57
|
+
groupedEnv[key][subKey] = processedEnv[eKey];
|
|
40
58
|
}
|
|
41
59
|
}
|
|
42
60
|
}
|
|
43
|
-
// Micro-optimization: avoid creating intermediate arrays from Object.entries
|
|
44
61
|
const schemaKeys = Object.keys(this.schema);
|
|
45
62
|
for (let i = 0; i < schemaKeys.length; i++) {
|
|
46
63
|
const key = schemaKeys[i];
|
|
47
64
|
const rule = this.schema[key];
|
|
65
|
+
// Keys that already have a preprocessing error (encryption mismatch, decryption failure, etc.) get skipped
|
|
66
|
+
if (skipKeys.has(key))
|
|
67
|
+
continue;
|
|
48
68
|
try {
|
|
49
69
|
// If we have grouped values for this key use them (preferred over JSON string)
|
|
50
70
|
const valToValidate = groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0
|
|
51
71
|
? groupedEnv[key]
|
|
52
|
-
:
|
|
72
|
+
: processedEnv[key];
|
|
53
73
|
// If both grouped and a top-level JSON value exist, prefer grouped and warn
|
|
54
|
-
if (groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 &&
|
|
74
|
+
if (groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 && processedEnv[key] !== undefined) {
|
|
55
75
|
console.warn(`Both prefixed variables and top-level ${key} exist; prefixed vars are used`);
|
|
56
76
|
}
|
|
57
77
|
// If strict mode is enabled, and this key has grouped env vars, ensure there are no unexpected subkeys
|
|
@@ -77,7 +97,7 @@ export class EnvValidator {
|
|
|
77
97
|
// - includeRaw: include raw values for non-sensitive fields
|
|
78
98
|
// - includeSensitive: when used with includeRaw, include raw sensitive values too (use with caution)
|
|
79
99
|
let displayedValue;
|
|
80
|
-
if (
|
|
100
|
+
if (processedEnv[key] === undefined) {
|
|
81
101
|
displayedValue = undefined;
|
|
82
102
|
}
|
|
83
103
|
else if (this.options?.includeRaw) {
|
|
@@ -85,11 +105,11 @@ export class EnvValidator {
|
|
|
85
105
|
displayedValue = "****";
|
|
86
106
|
}
|
|
87
107
|
else {
|
|
88
|
-
displayedValue =
|
|
108
|
+
displayedValue = processedEnv[key];
|
|
89
109
|
}
|
|
90
110
|
}
|
|
91
111
|
else {
|
|
92
|
-
displayedValue = this.redactValue(
|
|
112
|
+
displayedValue = this.redactValue(processedEnv[key], rule.sensitive);
|
|
93
113
|
}
|
|
94
114
|
this.errors.push({
|
|
95
115
|
key,
|
|
@@ -123,7 +143,17 @@ export class EnvValidator {
|
|
|
123
143
|
}
|
|
124
144
|
return result;
|
|
125
145
|
}
|
|
126
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Redacts a value to hide its contents, following these rules:
|
|
148
|
+
* - If `value` is undefined, returns undefined.
|
|
149
|
+
* - If `sensitive` is true, returns `"****"`.
|
|
150
|
+
* - If `value` is not a string, returns the original value.
|
|
151
|
+
* - If `value` is a string longer than 64 characters, truncates it to 4 characters
|
|
152
|
+
* at the start and end, and replaces the middle with `"..."`.
|
|
153
|
+
* - Otherwise, returns the original string.
|
|
154
|
+
* @param value The value to redact.
|
|
155
|
+
* @param sensitive If true, redacts the value to `"****"`.
|
|
156
|
+
*/
|
|
127
157
|
redactValue(value, sensitive) {
|
|
128
158
|
if (value === undefined)
|
|
129
159
|
return undefined;
|
|
@@ -136,8 +166,16 @@ export class EnvValidator {
|
|
|
136
166
|
}
|
|
137
167
|
return value;
|
|
138
168
|
}
|
|
139
|
-
|
|
140
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Tries to parse the given value as a JSON object.
|
|
171
|
+
* Returns `{ ok: true, value: JSON.parse(s) }` if successful,
|
|
172
|
+
* or `{ ok: false }` if not.
|
|
173
|
+
* The following conditions will cause the function to return `{ ok: false }`:
|
|
174
|
+
* - `value` is not a string
|
|
175
|
+
* - `value` is an empty string
|
|
176
|
+
* - `value` does not start with one of the following characters: `{`, `[`, `"`, `t`, `f`, `n`, or a digit
|
|
177
|
+
* - `value` cannot be parsed as a JSON object
|
|
178
|
+
*/
|
|
141
179
|
tryParseJSON(value) {
|
|
142
180
|
if (typeof value !== "string")
|
|
143
181
|
return { ok: false };
|
|
@@ -162,12 +200,22 @@ export class EnvValidator {
|
|
|
162
200
|
throw new Error(`Missing required environment variable`);
|
|
163
201
|
return effectiveRule.default;
|
|
164
202
|
}
|
|
203
|
+
if (typeof value === "string") {
|
|
204
|
+
value = value.trim();
|
|
205
|
+
// Re-check emptiness after trim in case the value was only whitespace
|
|
206
|
+
if (value === "") {
|
|
207
|
+
if (effectiveRule.required)
|
|
208
|
+
throw new Error(`Missing required environment variable`);
|
|
209
|
+
return effectiveRule.default;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
165
212
|
if (effectiveRule.transform) {
|
|
166
213
|
value = effectiveRule.transform(value);
|
|
167
214
|
}
|
|
168
215
|
switch (effectiveRule.type) {
|
|
169
216
|
case "string":
|
|
170
|
-
value
|
|
217
|
+
// value is already trimmed above; cast to string for minLength/maxLength checks
|
|
218
|
+
value = String(value);
|
|
171
219
|
if (effectiveRule.minLength !== undefined && value.length < effectiveRule.minLength) {
|
|
172
220
|
throw new Error(`Environment variable ${key} must be at least ${effectiveRule.minLength} characters`);
|
|
173
221
|
}
|
|
@@ -190,10 +238,10 @@ export class EnvValidator {
|
|
|
190
238
|
case "boolean":
|
|
191
239
|
if (typeof value === "string") {
|
|
192
240
|
value = value.toLowerCase();
|
|
193
|
-
if (value === "true") {
|
|
241
|
+
if (value === "true" || value === "yes" || value === "1" || value === "on") {
|
|
194
242
|
value = true;
|
|
195
243
|
}
|
|
196
|
-
else if (value === "false") {
|
|
244
|
+
else if (value === "false" || value === "no" || value === "0" || value === "off") {
|
|
197
245
|
value = false;
|
|
198
246
|
}
|
|
199
247
|
}
|
|
@@ -218,12 +266,12 @@ export class EnvValidator {
|
|
|
218
266
|
}
|
|
219
267
|
break;
|
|
220
268
|
case "email":
|
|
221
|
-
if (
|
|
269
|
+
if (!this.validateEmail(value)) {
|
|
222
270
|
throw new Error("Must be a valid email");
|
|
223
271
|
}
|
|
224
272
|
break;
|
|
225
273
|
case "ip":
|
|
226
|
-
if (
|
|
274
|
+
if (!net.isIP(value)) {
|
|
227
275
|
throw new Error("Must be a valid IP address");
|
|
228
276
|
}
|
|
229
277
|
break;
|
|
@@ -300,9 +348,104 @@ export class EnvValidator {
|
|
|
300
348
|
}
|
|
301
349
|
return value;
|
|
302
350
|
}
|
|
303
|
-
getEffectiveRule(
|
|
351
|
+
getEffectiveRule(_key, rule) {
|
|
304
352
|
const envName = process.env.NODE_ENV || "development";
|
|
305
353
|
const envRule = rule.env?.[envName] || {};
|
|
306
354
|
return { ...rule, ...envRule };
|
|
307
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Preprocesses environment variables to detect and handle encrypted values.
|
|
358
|
+
* 1. Decrypts any encrypted values using the private key.
|
|
359
|
+
* 2. Checks if any plaintext values are present for fields that should be encrypted.
|
|
360
|
+
* 3. Flags any env value that looks encrypted but the schema doesn't declare encrypted: true.
|
|
361
|
+
* @returns An object containing the processed environment variables and a set of keys that were skipped due to errors.
|
|
362
|
+
*/
|
|
363
|
+
preprocessEncryption(env) {
|
|
364
|
+
const processedEnv = { ...env };
|
|
365
|
+
const skipKeys = new Set();
|
|
366
|
+
const encryptedSchemaKeys = Object.keys(this.schema).filter((k) => this.schema[k].encrypted);
|
|
367
|
+
if (encryptedSchemaKeys.length > 0) {
|
|
368
|
+
// Only load the private key when at least one value is already encrypted
|
|
369
|
+
const needsDecryption = encryptedSchemaKeys.some((k) => processedEnv[k] != null && processedEnv[k] !== "" && isEncryptedValue(processedEnv[k]));
|
|
370
|
+
let privateKeyHex = null;
|
|
371
|
+
if (needsDecryption) {
|
|
372
|
+
privateKeyHex = loadPrivateKey({ keysPath: this.options?.keysPath });
|
|
373
|
+
if (!privateKeyHex) {
|
|
374
|
+
throw new EncryptionKeyMissingError("decryption");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (const key of encryptedSchemaKeys) {
|
|
378
|
+
const value = processedEnv[key];
|
|
379
|
+
if (value == null || value === "")
|
|
380
|
+
continue; // handled by required check in main loop
|
|
381
|
+
if (isEncryptedValue(value)) {
|
|
382
|
+
try {
|
|
383
|
+
processedEnv[key] = decryptEnvValue(value, privateKeyHex, key);
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
this.errors.push({
|
|
387
|
+
key,
|
|
388
|
+
message: err instanceof Error ? err.message : "Decryption failed",
|
|
389
|
+
rule: this.schema[key],
|
|
390
|
+
});
|
|
391
|
+
skipKeys.add(key);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// Plaintext value for a field that should be encrypted
|
|
396
|
+
if (this.options?.allowPlaintext) {
|
|
397
|
+
console.warn(`[dotenv-gad] "${key}" has a plaintext value but schema declares encrypted: true. ` +
|
|
398
|
+
"Run: npx dotenv-gad encrypt");
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
this.errors.push({
|
|
402
|
+
key,
|
|
403
|
+
message: 'Must be encrypted. Run: npx dotenv-gad encrypt',
|
|
404
|
+
rule: this.schema[key],
|
|
405
|
+
});
|
|
406
|
+
skipKeys.add(key);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Flag any env value that looks encrypted but the schema doesn't declare encrypted: true
|
|
412
|
+
for (const [eKey, value] of Object.entries(processedEnv)) {
|
|
413
|
+
if (skipKeys.has(eKey))
|
|
414
|
+
continue;
|
|
415
|
+
if (value && isEncryptedValue(value) && !this.schema[eKey]?.encrypted) {
|
|
416
|
+
this.errors.push({
|
|
417
|
+
key: eKey,
|
|
418
|
+
message: "Encrypted value found but schema does not declare encrypted: true",
|
|
419
|
+
});
|
|
420
|
+
skipKeys.add(eKey);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return { processedEnv, skipKeys };
|
|
424
|
+
}
|
|
425
|
+
/*
|
|
426
|
+
* Email validation logic adapted from:
|
|
427
|
+
* Project: email-validator
|
|
428
|
+
* * Repository: https://github.com/manishsaraan/email-validator
|
|
429
|
+
*
|
|
430
|
+
* * Adapted to enforce email validation rules more strictly
|
|
431
|
+
*/
|
|
432
|
+
validateEmail(email) {
|
|
433
|
+
if (!email)
|
|
434
|
+
return false;
|
|
435
|
+
const emailParts = email.split("@");
|
|
436
|
+
if (emailParts.length !== 2)
|
|
437
|
+
return false;
|
|
438
|
+
const [local, domain] = emailParts;
|
|
439
|
+
if (local.length > EnvValidator.LOCAL_MAX || domain.length > EnvValidator.DOMAIN_MAX)
|
|
440
|
+
return false;
|
|
441
|
+
const domainParts = domain.split(".");
|
|
442
|
+
if (domainParts.some(part => part.length > EnvValidator.DOMAIN_PART_MAX))
|
|
443
|
+
return false;
|
|
444
|
+
return EnvValidator.EMAIL_REGEX.test(email);
|
|
445
|
+
}
|
|
308
446
|
}
|
|
447
|
+
Object.defineProperty(EnvValidator, "name", {
|
|
448
|
+
value: "EnvValidator",
|
|
449
|
+
configurable: true,
|
|
450
|
+
writable: false,
|
|
451
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotenv-gad",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/types/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -100,7 +100,6 @@
|
|
|
100
100
|
"commander": "^14.0.2",
|
|
101
101
|
"dotenv": "^17.2.3",
|
|
102
102
|
"figlet": "^1.10.0",
|
|
103
|
-
"inquirer": "^13.2.1",
|
|
104
103
|
"ora": "^9.1.0"
|
|
105
104
|
}
|
|
106
105
|
}
|