dotenv-gad 1.4.2 → 1.6.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 +81 -11
- 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 +100 -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 +40 -28
- package/dist/cli/commands/verify.js +101 -0
- package/dist/cli/index.js +12 -0
- package/dist/crypto.js +165 -0
- package/dist/errors.js +92 -4
- package/dist/index.cjs +408 -26
- package/dist/index.js +6 -4
- package/dist/runtime.js +43 -0
- package/dist/schema-loader.js +12 -3
- 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 +8 -3
- package/dist/types/runtime.d.ts +29 -0
- package/dist/types/schema-loader.d.ts +5 -2
- package/dist/types/schema.d.ts +7 -0
- package/dist/types/utils.d.ts +21 -10
- package/dist/types/validator.d.ts +40 -0
- package/dist/utils.js +50 -13
- package/dist/validator.js +164 -19
- package/package.json +6 -4
package/dist/validator.js
CHANGED
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
import { EnvAggregateError } from "./errors.js";
|
|
1
|
+
import { EnvAggregateError, EncryptionKeyMissingError } from "./errors.js";
|
|
2
|
+
import { decryptEnvValue, isEncryptedValue, loadPrivateKey } from "./crypto.js";
|
|
3
|
+
import { getEnv } from "./runtime.js";
|
|
4
|
+
import net from "net";
|
|
5
|
+
const kValidator = Symbol.for("dotenv-gad.EnvValidator");
|
|
2
6
|
export class EnvValidator {
|
|
3
7
|
schema;
|
|
4
8
|
options;
|
|
9
|
+
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])+$/;
|
|
10
|
+
static LOCAL_MAX = 64;
|
|
11
|
+
static DOMAIN_MAX = 255;
|
|
12
|
+
static DOMAIN_PART_MAX = 63;
|
|
5
13
|
errors = [];
|
|
6
14
|
/**
|
|
7
15
|
* Constructs a new EnvValidator instance.
|
|
8
16
|
* @param {SchemaDefinition} schema The schema definition for the environment variables.
|
|
9
17
|
* @param {Object} [options] Optional options for the validation process.
|
|
10
18
|
* @param {boolean} [options.strict] When true, environment variables not present in the schema will be rejected.
|
|
19
|
+
* @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.
|
|
20
|
+
* @param {string} [options.keysPath] Path to the `.env.keys` file (default: `.env.keys`).
|
|
11
21
|
*/
|
|
12
22
|
constructor(schema, options) {
|
|
13
23
|
this.schema = schema;
|
|
14
24
|
this.options = options;
|
|
25
|
+
Object.defineProperty(this, kValidator, { value: true, enumerable: false });
|
|
26
|
+
}
|
|
27
|
+
/** Bundler-safe instanceof — checks the symbol marker, not the class name. */
|
|
28
|
+
static [Symbol.hasInstance](instance) {
|
|
29
|
+
return (typeof instance === "object" &&
|
|
30
|
+
instance !== null &&
|
|
31
|
+
Object.prototype.hasOwnProperty.call(instance, kValidator));
|
|
15
32
|
}
|
|
16
33
|
validate(env) {
|
|
17
34
|
this.errors = [];
|
|
35
|
+
// First Decrypt encrypted fields and check for schema/value mismatches.
|
|
36
|
+
const { processedEnv, skipKeys } = this.preprocessEncryption(env);
|
|
18
37
|
const result = {};
|
|
19
38
|
// Build grouping map for object types that support envPrefix.
|
|
20
39
|
// We'll collect all prefixes first and then make a single pass over env keys
|
|
@@ -29,29 +48,31 @@ export class EnvValidator {
|
|
|
29
48
|
groupedEnv[k] = {};
|
|
30
49
|
}
|
|
31
50
|
}
|
|
32
|
-
const envKeys = Object.keys(
|
|
51
|
+
const envKeys = Object.keys(processedEnv);
|
|
33
52
|
for (let i = 0; i < envKeys.length; i++) {
|
|
34
53
|
const eKey = envKeys[i];
|
|
35
54
|
for (let j = 0; j < prefixes.length; j++) {
|
|
36
55
|
const { key, prefix } = prefixes[j];
|
|
37
56
|
if (eKey.startsWith(prefix)) {
|
|
38
57
|
const subKey = eKey.slice(prefix.length);
|
|
39
|
-
groupedEnv[key][subKey] =
|
|
58
|
+
groupedEnv[key][subKey] = processedEnv[eKey];
|
|
40
59
|
}
|
|
41
60
|
}
|
|
42
61
|
}
|
|
43
|
-
// Micro-optimization: avoid creating intermediate arrays from Object.entries
|
|
44
62
|
const schemaKeys = Object.keys(this.schema);
|
|
45
63
|
for (let i = 0; i < schemaKeys.length; i++) {
|
|
46
64
|
const key = schemaKeys[i];
|
|
47
65
|
const rule = this.schema[key];
|
|
66
|
+
// Keys that already have a preprocessing error (encryption mismatch, decryption failure, etc.) get skipped
|
|
67
|
+
if (skipKeys.has(key))
|
|
68
|
+
continue;
|
|
48
69
|
try {
|
|
49
70
|
// If we have grouped values for this key use them (preferred over JSON string)
|
|
50
71
|
const valToValidate = groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0
|
|
51
72
|
? groupedEnv[key]
|
|
52
|
-
:
|
|
73
|
+
: processedEnv[key];
|
|
53
74
|
// If both grouped and a top-level JSON value exist, prefer grouped and warn
|
|
54
|
-
if (groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 &&
|
|
75
|
+
if (groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 && processedEnv[key] !== undefined) {
|
|
55
76
|
console.warn(`Both prefixed variables and top-level ${key} exist; prefixed vars are used`);
|
|
56
77
|
}
|
|
57
78
|
// If strict mode is enabled, and this key has grouped env vars, ensure there are no unexpected subkeys
|
|
@@ -77,7 +98,7 @@ export class EnvValidator {
|
|
|
77
98
|
// - includeRaw: include raw values for non-sensitive fields
|
|
78
99
|
// - includeSensitive: when used with includeRaw, include raw sensitive values too (use with caution)
|
|
79
100
|
let displayedValue;
|
|
80
|
-
if (
|
|
101
|
+
if (processedEnv[key] === undefined) {
|
|
81
102
|
displayedValue = undefined;
|
|
82
103
|
}
|
|
83
104
|
else if (this.options?.includeRaw) {
|
|
@@ -85,11 +106,11 @@ export class EnvValidator {
|
|
|
85
106
|
displayedValue = "****";
|
|
86
107
|
}
|
|
87
108
|
else {
|
|
88
|
-
displayedValue =
|
|
109
|
+
displayedValue = processedEnv[key];
|
|
89
110
|
}
|
|
90
111
|
}
|
|
91
112
|
else {
|
|
92
|
-
displayedValue = this.redactValue(
|
|
113
|
+
displayedValue = this.redactValue(processedEnv[key], rule.sensitive);
|
|
93
114
|
}
|
|
94
115
|
this.errors.push({
|
|
95
116
|
key,
|
|
@@ -123,7 +144,17 @@ export class EnvValidator {
|
|
|
123
144
|
}
|
|
124
145
|
return result;
|
|
125
146
|
}
|
|
126
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Redacts a value to hide its contents, following these rules:
|
|
149
|
+
* - If `value` is undefined, returns undefined.
|
|
150
|
+
* - If `sensitive` is true, returns `"****"`.
|
|
151
|
+
* - If `value` is not a string, returns the original value.
|
|
152
|
+
* - If `value` is a string longer than 64 characters, truncates it to 4 characters
|
|
153
|
+
* at the start and end, and replaces the middle with `"..."`.
|
|
154
|
+
* - Otherwise, returns the original string.
|
|
155
|
+
* @param value The value to redact.
|
|
156
|
+
* @param sensitive If true, redacts the value to `"****"`.
|
|
157
|
+
*/
|
|
127
158
|
redactValue(value, sensitive) {
|
|
128
159
|
if (value === undefined)
|
|
129
160
|
return undefined;
|
|
@@ -136,8 +167,16 @@ export class EnvValidator {
|
|
|
136
167
|
}
|
|
137
168
|
return value;
|
|
138
169
|
}
|
|
139
|
-
|
|
140
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Tries to parse the given value as a JSON object.
|
|
172
|
+
* Returns `{ ok: true, value: JSON.parse(s) }` if successful,
|
|
173
|
+
* or `{ ok: false }` if not.
|
|
174
|
+
* The following conditions will cause the function to return `{ ok: false }`:
|
|
175
|
+
* - `value` is not a string
|
|
176
|
+
* - `value` is an empty string
|
|
177
|
+
* - `value` does not start with one of the following characters: `{`, `[`, `"`, `t`, `f`, `n`, or a digit
|
|
178
|
+
* - `value` cannot be parsed as a JSON object
|
|
179
|
+
*/
|
|
141
180
|
tryParseJSON(value) {
|
|
142
181
|
if (typeof value !== "string")
|
|
143
182
|
return { ok: false };
|
|
@@ -162,12 +201,22 @@ export class EnvValidator {
|
|
|
162
201
|
throw new Error(`Missing required environment variable`);
|
|
163
202
|
return effectiveRule.default;
|
|
164
203
|
}
|
|
204
|
+
if (typeof value === "string") {
|
|
205
|
+
value = value.trim();
|
|
206
|
+
// Re-check emptiness after trim in case the value was only whitespace
|
|
207
|
+
if (value === "") {
|
|
208
|
+
if (effectiveRule.required)
|
|
209
|
+
throw new Error(`Missing required environment variable`);
|
|
210
|
+
return effectiveRule.default;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
165
213
|
if (effectiveRule.transform) {
|
|
166
214
|
value = effectiveRule.transform(value);
|
|
167
215
|
}
|
|
168
216
|
switch (effectiveRule.type) {
|
|
169
217
|
case "string":
|
|
170
|
-
value
|
|
218
|
+
// value is already trimmed above; cast to string for minLength/maxLength checks
|
|
219
|
+
value = String(value);
|
|
171
220
|
if (effectiveRule.minLength !== undefined && value.length < effectiveRule.minLength) {
|
|
172
221
|
throw new Error(`Environment variable ${key} must be at least ${effectiveRule.minLength} characters`);
|
|
173
222
|
}
|
|
@@ -190,10 +239,10 @@ export class EnvValidator {
|
|
|
190
239
|
case "boolean":
|
|
191
240
|
if (typeof value === "string") {
|
|
192
241
|
value = value.toLowerCase();
|
|
193
|
-
if (value === "true") {
|
|
242
|
+
if (value === "true" || value === "yes" || value === "1" || value === "on") {
|
|
194
243
|
value = true;
|
|
195
244
|
}
|
|
196
|
-
else if (value === "false") {
|
|
245
|
+
else if (value === "false" || value === "no" || value === "0" || value === "off") {
|
|
197
246
|
value = false;
|
|
198
247
|
}
|
|
199
248
|
}
|
|
@@ -218,12 +267,12 @@ export class EnvValidator {
|
|
|
218
267
|
}
|
|
219
268
|
break;
|
|
220
269
|
case "email":
|
|
221
|
-
if (
|
|
270
|
+
if (!this.validateEmail(value)) {
|
|
222
271
|
throw new Error("Must be a valid email");
|
|
223
272
|
}
|
|
224
273
|
break;
|
|
225
274
|
case "ip":
|
|
226
|
-
if (
|
|
275
|
+
if (!net.isIP(value)) {
|
|
227
276
|
throw new Error("Must be a valid IP address");
|
|
228
277
|
}
|
|
229
278
|
break;
|
|
@@ -300,9 +349,105 @@ export class EnvValidator {
|
|
|
300
349
|
}
|
|
301
350
|
return value;
|
|
302
351
|
}
|
|
303
|
-
getEffectiveRule(
|
|
304
|
-
const
|
|
352
|
+
getEffectiveRule(_key, rule) {
|
|
353
|
+
const runtimeEnv = getEnv();
|
|
354
|
+
const envName = runtimeEnv.NODE_ENV || "development";
|
|
305
355
|
const envRule = rule.env?.[envName] || {};
|
|
306
356
|
return { ...rule, ...envRule };
|
|
307
357
|
}
|
|
358
|
+
/**
|
|
359
|
+
* Preprocesses environment variables to detect and handle encrypted values.
|
|
360
|
+
* 1. Decrypts any encrypted values using the private key.
|
|
361
|
+
* 2. Checks if any plaintext values are present for fields that should be encrypted.
|
|
362
|
+
* 3. Flags any env value that looks encrypted but the schema doesn't declare encrypted: true.
|
|
363
|
+
* @returns An object containing the processed environment variables and a set of keys that were skipped due to errors.
|
|
364
|
+
*/
|
|
365
|
+
preprocessEncryption(env) {
|
|
366
|
+
const processedEnv = { ...env };
|
|
367
|
+
const skipKeys = new Set();
|
|
368
|
+
const encryptedSchemaKeys = Object.keys(this.schema).filter((k) => this.schema[k].encrypted);
|
|
369
|
+
if (encryptedSchemaKeys.length > 0) {
|
|
370
|
+
// Only load the private key when at least one value is already encrypted
|
|
371
|
+
const needsDecryption = encryptedSchemaKeys.some((k) => processedEnv[k] != null && processedEnv[k] !== "" && isEncryptedValue(processedEnv[k]));
|
|
372
|
+
let privateKeyHex = null;
|
|
373
|
+
if (needsDecryption) {
|
|
374
|
+
privateKeyHex = loadPrivateKey({ keysPath: this.options?.keysPath });
|
|
375
|
+
if (!privateKeyHex) {
|
|
376
|
+
throw new EncryptionKeyMissingError("decryption");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (const key of encryptedSchemaKeys) {
|
|
380
|
+
const value = processedEnv[key];
|
|
381
|
+
if (value == null || value === "")
|
|
382
|
+
continue; // handled by required check in main loop
|
|
383
|
+
if (isEncryptedValue(value)) {
|
|
384
|
+
try {
|
|
385
|
+
processedEnv[key] = decryptEnvValue(value, privateKeyHex, key);
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
this.errors.push({
|
|
389
|
+
key,
|
|
390
|
+
message: err instanceof Error ? err.message : "Decryption failed",
|
|
391
|
+
rule: this.schema[key],
|
|
392
|
+
});
|
|
393
|
+
skipKeys.add(key);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// Plaintext value for a field that should be encrypted
|
|
398
|
+
if (this.options?.allowPlaintext) {
|
|
399
|
+
console.warn(`[dotenv-gad] "${key}" has a plaintext value but schema declares encrypted: true. ` +
|
|
400
|
+
"Run: npx dotenv-gad encrypt");
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
this.errors.push({
|
|
404
|
+
key,
|
|
405
|
+
message: 'Must be encrypted. Run: npx dotenv-gad encrypt',
|
|
406
|
+
rule: this.schema[key],
|
|
407
|
+
});
|
|
408
|
+
skipKeys.add(key);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Flag any env value that looks encrypted but the schema doesn't declare encrypted: true
|
|
414
|
+
for (const [eKey, value] of Object.entries(processedEnv)) {
|
|
415
|
+
if (skipKeys.has(eKey))
|
|
416
|
+
continue;
|
|
417
|
+
if (value && isEncryptedValue(value) && !this.schema[eKey]?.encrypted) {
|
|
418
|
+
this.errors.push({
|
|
419
|
+
key: eKey,
|
|
420
|
+
message: "Encrypted value found but schema does not declare encrypted: true",
|
|
421
|
+
});
|
|
422
|
+
skipKeys.add(eKey);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { processedEnv, skipKeys };
|
|
426
|
+
}
|
|
427
|
+
/*
|
|
428
|
+
* Email validation logic adapted from:
|
|
429
|
+
* Project: email-validator
|
|
430
|
+
* * Repository: https://github.com/manishsaraan/email-validator
|
|
431
|
+
*
|
|
432
|
+
* * Adapted to enforce email validation rules more strictly
|
|
433
|
+
*/
|
|
434
|
+
validateEmail(email) {
|
|
435
|
+
if (!email)
|
|
436
|
+
return false;
|
|
437
|
+
const emailParts = email.split("@");
|
|
438
|
+
if (emailParts.length !== 2)
|
|
439
|
+
return false;
|
|
440
|
+
const [local, domain] = emailParts;
|
|
441
|
+
if (local.length > EnvValidator.LOCAL_MAX || domain.length > EnvValidator.DOMAIN_MAX)
|
|
442
|
+
return false;
|
|
443
|
+
const domainParts = domain.split(".");
|
|
444
|
+
if (domainParts.some(part => part.length > EnvValidator.DOMAIN_PART_MAX))
|
|
445
|
+
return false;
|
|
446
|
+
return EnvValidator.EMAIL_REGEX.test(email);
|
|
447
|
+
}
|
|
308
448
|
}
|
|
449
|
+
Object.defineProperty(EnvValidator, "name", {
|
|
450
|
+
value: "EnvValidator",
|
|
451
|
+
configurable: true,
|
|
452
|
+
writable: false,
|
|
453
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dotenv-gad",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/types/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -32,10 +32,12 @@
|
|
|
32
32
|
"config",
|
|
33
33
|
"type-safe",
|
|
34
34
|
"nodejs",
|
|
35
|
+
"bun",
|
|
35
36
|
"cli"
|
|
36
37
|
],
|
|
37
38
|
"engines": {
|
|
38
|
-
"node": ">=18.0.0"
|
|
39
|
+
"node": ">=18.0.0",
|
|
40
|
+
"bun": ">=1.0.0"
|
|
39
41
|
},
|
|
40
42
|
"exports": {
|
|
41
43
|
".": {
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
],
|
|
64
66
|
"author": "Kasim Lyee",
|
|
65
67
|
"license": "MIT",
|
|
66
|
-
"description": "Environment variable validation and type safety for Node.js and modern JavaScript applications",
|
|
68
|
+
"description": "Environment variable validation and type safety for Node.js, Bun, and modern JavaScript applications",
|
|
67
69
|
"repository": {
|
|
68
70
|
"type": "git",
|
|
69
71
|
"url": "https://github.com/kasimlyee/dotenv-gad.git"
|
|
@@ -96,11 +98,11 @@
|
|
|
96
98
|
"esbuild": "^0.27.2"
|
|
97
99
|
},
|
|
98
100
|
"dependencies": {
|
|
101
|
+
"@noble/ciphers": "2.2.0",
|
|
99
102
|
"chalk": "^5.6.2",
|
|
100
103
|
"commander": "^14.0.2",
|
|
101
104
|
"dotenv": "^17.2.3",
|
|
102
105
|
"figlet": "^1.10.0",
|
|
103
|
-
"inquirer": "^13.2.1",
|
|
104
106
|
"ora": "^9.1.0"
|
|
105
107
|
}
|
|
106
108
|
}
|