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.
Files changed (38) hide show
  1. package/README.md +47 -10
  2. package/dist/cli/commands/check.js +10 -2
  3. package/dist/cli/commands/decrypt.js +126 -0
  4. package/dist/cli/commands/docs.js +2 -2
  5. package/dist/cli/commands/encrypt.js +94 -0
  6. package/dist/cli/commands/fix.js +16 -12
  7. package/dist/cli/commands/init.js +17 -12
  8. package/dist/cli/commands/keygen.js +97 -0
  9. package/dist/cli/commands/rotate.js +160 -0
  10. package/dist/cli/commands/status.js +116 -0
  11. package/dist/cli/commands/sync.js +15 -17
  12. package/dist/cli/commands/utils.js +34 -28
  13. package/dist/cli/commands/verify.js +101 -0
  14. package/dist/cli/index.js +12 -0
  15. package/dist/crypto.js +162 -0
  16. package/dist/errors.js +92 -4
  17. package/dist/index.cjs +375 -24
  18. package/dist/index.js +5 -4
  19. package/dist/types/cli/commands/check.d.ts +1 -1
  20. package/dist/types/cli/commands/decrypt.d.ts +2 -0
  21. package/dist/types/cli/commands/docs.d.ts +1 -1
  22. package/dist/types/cli/commands/encrypt.d.ts +2 -0
  23. package/dist/types/cli/commands/fix.d.ts +1 -1
  24. package/dist/types/cli/commands/init.d.ts +1 -1
  25. package/dist/types/cli/commands/keygen.d.ts +2 -0
  26. package/dist/types/cli/commands/rotate.d.ts +2 -0
  27. package/dist/types/cli/commands/status.d.ts +2 -0
  28. package/dist/types/cli/commands/sync.d.ts +1 -1
  29. package/dist/types/cli/commands/verify.d.ts +2 -0
  30. package/dist/types/crypto.d.ts +58 -0
  31. package/dist/types/errors.d.ts +35 -12
  32. package/dist/types/index.d.ts +7 -3
  33. package/dist/types/schema.d.ts +7 -0
  34. package/dist/types/utils.d.ts +19 -10
  35. package/dist/types/validator.d.ts +40 -0
  36. package/dist/utils.js +30 -12
  37. package/dist/validator.js +161 -18
  38. package/package.json +1 -2
package/dist/index.cjs CHANGED
@@ -31,18 +31,28 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  AggregateError: () => AggregateError,
34
+ DecryptionFailedError: () => DecryptionFailedError,
35
+ EncryptedFieldMismatchError: () => EncryptedFieldMismatchError,
36
+ EncryptionKeyMissingError: () => EncryptionKeyMissingError,
34
37
  EnvAggregateError: () => EnvAggregateError,
35
38
  EnvValidationError: () => EnvValidationError,
36
39
  EnvValidator: () => EnvValidator,
37
40
  composeSchema: () => composeSchema,
38
41
  createEnvProxy: () => createEnvProxy,
42
+ decryptEnvValue: () => decryptEnvValue,
39
43
  defineSchema: () => defineSchema,
44
+ encryptEnvValue: () => encryptEnvValue,
45
+ generateKeyPair: () => generateKeyPair,
46
+ isEncryptedValue: () => isEncryptedValue,
40
47
  loadEnv: () => loadEnv,
48
+ loadPrivateKey: () => loadPrivateKey,
41
49
  validateEnv: () => validateEnv
42
50
  });
43
51
  module.exports = __toCommonJS(src_exports);
44
52
 
45
53
  // src/errors.ts
54
+ var kValidationError = /* @__PURE__ */ Symbol.for("dotenv-gad.EnvValidationError");
55
+ var kAggregateError = /* @__PURE__ */ Symbol.for("dotenv-gad.EnvAggregateError");
46
56
  var EnvValidationError = class extends Error {
47
57
  constructor(key, message, receiveValue) {
48
58
  super(message);
@@ -50,15 +60,40 @@ var EnvValidationError = class extends Error {
50
60
  this.message = message;
51
61
  this.receiveValue = receiveValue;
52
62
  this.name = "EnvValidationError";
63
+ Object.defineProperty(this, kValidationError, { value: true, enumerable: false });
64
+ }
65
+ static [Symbol.hasInstance](instance) {
66
+ return typeof instance === "object" && instance !== null && Object.prototype.hasOwnProperty.call(instance, kValidationError);
53
67
  }
54
68
  };
69
+ Object.defineProperty(EnvValidationError, "name", {
70
+ value: "EnvValidationError",
71
+ configurable: true,
72
+ writable: false
73
+ });
74
+ var errorsMap = /* @__PURE__ */ new WeakMap();
55
75
  var EnvAggregateError = class _EnvAggregateError extends Error {
76
+ get errors() {
77
+ return errorsMap.get(this);
78
+ }
56
79
  constructor(errors, message) {
57
- super(message);
58
- this.errors = errors;
80
+ const summary = errors.map((e) => {
81
+ let line = `
82
+ - ${e.key}: ${e.message}`;
83
+ if (e.value !== void 0) line += ` (received: ${JSON.stringify(e.value)})`;
84
+ if (e.rule?.docs) line += `
85
+ hint: ${e.rule.docs}`;
86
+ return line;
87
+ }).join("");
88
+ super(message + summary);
59
89
  this.name = "EnvAggregateError";
90
+ errorsMap.set(this, errors);
91
+ Object.defineProperty(this, kAggregateError, { value: true, enumerable: false });
60
92
  Object.setPrototypeOf(this, _EnvAggregateError.prototype);
61
93
  }
94
+ static [Symbol.hasInstance](instance) {
95
+ return typeof instance === "object" && instance !== null && Object.prototype.hasOwnProperty.call(instance, kAggregateError);
96
+ }
62
97
  toString() {
63
98
  const errorList = this.errors.map((e) => {
64
99
  let msg = ` - ${e.key}: ${e.message}`;
@@ -68,27 +103,211 @@ var EnvAggregateError = class _EnvAggregateError extends Error {
68
103
  ${e.rule.docs}`;
69
104
  return msg;
70
105
  }).join("\n");
71
- return `${this.message}:
106
+ return `${this.name}: ${this.message}
72
107
  ${errorList}`;
73
108
  }
74
109
  };
110
+ Object.defineProperty(EnvAggregateError, "name", {
111
+ value: "EnvAggregateError",
112
+ configurable: true,
113
+ writable: false
114
+ });
75
115
  var AggregateError = EnvAggregateError;
116
+ var EncryptionKeyMissingError = class _EncryptionKeyMissingError extends Error {
117
+ constructor(context) {
118
+ const message = context === "encryption" ? "Public key not found. Run: npx dotenv-gad keygen" : "Private key not found. Obtain .env.keys from your team or set ENVGAD_PRIVATE_KEY env var. Run: npx dotenv-gad keygen to generate new keys.";
119
+ super(message);
120
+ this.name = "EncryptionKeyMissingError";
121
+ Object.setPrototypeOf(this, _EncryptionKeyMissingError.prototype);
122
+ }
123
+ };
124
+ var DecryptionFailedError = class _DecryptionFailedError extends Error {
125
+ constructor(varName) {
126
+ super(
127
+ `Decryption failed for "${varName}". Possible causes:
128
+ - Wrong private key
129
+ - Corrupted ciphertext
130
+ - Ciphertext moved from a different variable (AAD mismatch)`
131
+ );
132
+ this.name = "DecryptionFailedError";
133
+ Object.setPrototypeOf(this, _DecryptionFailedError.prototype);
134
+ }
135
+ };
136
+ var EncryptedFieldMismatchError = class _EncryptedFieldMismatchError extends Error {
137
+ constructor(varName, shouldBeEncrypted) {
138
+ const message = shouldBeEncrypted ? `"${varName}" must be encrypted but received a plaintext value. Run: npx dotenv-gad encrypt` : `"${varName}" has an encrypted value but schema does not declare encrypted: true`;
139
+ super(message);
140
+ this.name = "EncryptedFieldMismatchError";
141
+ Object.setPrototypeOf(this, _EncryptedFieldMismatchError.prototype);
142
+ }
143
+ };
144
+
145
+ // src/crypto.ts
146
+ var import_node_crypto = require("crypto");
147
+ var import_node_fs = require("fs");
148
+ var SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
149
+ var RAW_KEY_LENGTH = 32;
150
+ var NONCE_LENGTH = 12;
151
+ var AUTH_TAG_LENGTH = 16;
152
+ var PROTOCOL_PREFIX = "encrypted:";
153
+ var ENCRYPTION_VERSION = "v1";
154
+ var HKDF_INFO = Buffer.from("dotenv-gad:v1");
155
+ var HKDF_SALT = Buffer.alloc(0);
156
+ var AAD_PREFIX = "dotenv-gad:v1:";
157
+ var PRIVATE_KEY_HEX_LENGTH = 96;
158
+ var PUBLIC_KEY_HEX_LENGTH = 88;
159
+ function generateKeyPair() {
160
+ const { publicKey, privateKey } = (0, import_node_crypto.generateKeyPairSync)("x25519", {
161
+ publicKeyEncoding: { type: "spki", format: "der" },
162
+ privateKeyEncoding: { type: "pkcs8", format: "der" }
163
+ });
164
+ return {
165
+ publicKeyHex: publicKey.toString("hex"),
166
+ privateKeyHex: privateKey.toString("hex")
167
+ };
168
+ }
169
+ function encryptEnvValue(plaintext, recipientPublicKeyHex, varName) {
170
+ if (!/^[a-fA-F0-9]+$/.test(recipientPublicKeyHex) || recipientPublicKeyHex.length !== PUBLIC_KEY_HEX_LENGTH) {
171
+ throw new Error(
172
+ `Invalid ENVGAD_PUBLIC_KEY format (expected ${PUBLIC_KEY_HEX_LENGTH}-char hex-encoded SPKI DER, got ${recipientPublicKeyHex.length} chars)`
173
+ );
174
+ }
175
+ const recipientSpki = Buffer.from(recipientPublicKeyHex, "hex");
176
+ const { publicKeyHex: ephPubHex, privateKeyHex: ephPrivHex } = generateKeyPair();
177
+ const ephSpki = Buffer.from(ephPubHex, "hex");
178
+ const ephPkcs8 = Buffer.from(ephPrivHex, "hex");
179
+ const sharedSecret = (0, import_node_crypto.diffieHellman)({
180
+ privateKey: (0, import_node_crypto.createPrivateKey)({ key: ephPkcs8, format: "der", type: "pkcs8" }),
181
+ publicKey: (0, import_node_crypto.createPublicKey)({ key: recipientSpki, format: "der", type: "spki" })
182
+ });
183
+ const encKey = Buffer.from(
184
+ (0, import_node_crypto.hkdfSync)("sha256", sharedSecret, HKDF_SALT, HKDF_INFO, 32)
185
+ );
186
+ const nonce = (0, import_node_crypto.randomBytes)(NONCE_LENGTH);
187
+ const cipher = (0, import_node_crypto.createCipheriv)(
188
+ "chacha20-poly1305",
189
+ encKey,
190
+ nonce,
191
+ { authTagLength: AUTH_TAG_LENGTH }
192
+ );
193
+ cipher.setAAD(Buffer.from(`${AAD_PREFIX}${varName}`));
194
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
195
+ const authTag = cipher.getAuthTag();
196
+ const rawEphPubKey = ephSpki.subarray(SPKI_PREFIX.length);
197
+ const payload = Buffer.concat([rawEphPubKey, nonce, ciphertext, authTag]);
198
+ return `${PROTOCOL_PREFIX}${ENCRYPTION_VERSION}:${payload.toString("base64")}`;
199
+ }
200
+ function decryptEnvValue(token, privateKeyHex, varName) {
201
+ const prefix = `${PROTOCOL_PREFIX}${ENCRYPTION_VERSION}:`;
202
+ if (!token.startsWith(prefix)) {
203
+ const versionMatch = token.match(/^encrypted:(\w+):/);
204
+ if (versionMatch) {
205
+ throw new Error(
206
+ `Unsupported encryption version: ${versionMatch[1]}. This version of dotenv-gad supports: ${ENCRYPTION_VERSION}`
207
+ );
208
+ }
209
+ throw new Error("Invalid encrypted value format");
210
+ }
211
+ let payload;
212
+ try {
213
+ payload = Buffer.from(token.slice(prefix.length), "base64");
214
+ } catch {
215
+ throw new Error("Invalid base64 encoding in encrypted value");
216
+ }
217
+ const minLength = RAW_KEY_LENGTH + NONCE_LENGTH + AUTH_TAG_LENGTH;
218
+ if (payload.length < minLength) {
219
+ throw new Error(
220
+ `Encrypted value too short: ${payload.length} bytes (minimum: ${minLength})`
221
+ );
222
+ }
223
+ const rawEphPubKey = payload.subarray(0, RAW_KEY_LENGTH);
224
+ const nonce = payload.subarray(RAW_KEY_LENGTH, RAW_KEY_LENGTH + NONCE_LENGTH);
225
+ const rest = payload.subarray(RAW_KEY_LENGTH + NONCE_LENGTH);
226
+ const authTag = rest.subarray(rest.length - AUTH_TAG_LENGTH);
227
+ const ciphertext = rest.subarray(0, rest.length - AUTH_TAG_LENGTH);
228
+ const ephSpki = Buffer.concat([SPKI_PREFIX, rawEphPubKey]);
229
+ const pkcs8 = Buffer.from(privateKeyHex, "hex");
230
+ const sharedSecret = (0, import_node_crypto.diffieHellman)({
231
+ privateKey: (0, import_node_crypto.createPrivateKey)({ key: pkcs8, format: "der", type: "pkcs8" }),
232
+ publicKey: (0, import_node_crypto.createPublicKey)({ key: ephSpki, format: "der", type: "spki" })
233
+ });
234
+ const encKey = Buffer.from(
235
+ (0, import_node_crypto.hkdfSync)("sha256", sharedSecret, HKDF_SALT, HKDF_INFO, 32)
236
+ );
237
+ const decipher = (0, import_node_crypto.createDecipheriv)(
238
+ "chacha20-poly1305",
239
+ encKey,
240
+ nonce,
241
+ { authTagLength: AUTH_TAG_LENGTH }
242
+ );
243
+ decipher.setAAD(Buffer.from(`${AAD_PREFIX}${varName}`));
244
+ decipher.setAuthTag(authTag);
245
+ try {
246
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
247
+ return plaintext.toString("utf8");
248
+ } catch {
249
+ throw new DecryptionFailedError(varName);
250
+ }
251
+ }
252
+ function isEncryptedValue(value) {
253
+ return value.startsWith(`${PROTOCOL_PREFIX}${ENCRYPTION_VERSION}:`);
254
+ }
255
+ function loadPrivateKey(options = {}) {
256
+ const keysPath = options.keysPath ?? ".env.keys";
257
+ if ((0, import_node_fs.existsSync)(keysPath)) {
258
+ const content = (0, import_node_fs.readFileSync)(keysPath, "utf8");
259
+ const match = content.match(/^ENVGAD_PRIVATE_KEY=([a-fA-F0-9]+)/m);
260
+ if (match) {
261
+ const key = match[1];
262
+ if (key.length !== PRIVATE_KEY_HEX_LENGTH) {
263
+ throw new Error(
264
+ `Invalid ENVGAD_PRIVATE_KEY in ${keysPath}: expected ${PRIVATE_KEY_HEX_LENGTH}-char hex-encoded PKCS8 DER, got ${key.length} chars`
265
+ );
266
+ }
267
+ return key;
268
+ }
269
+ }
270
+ const envKey = process.env.ENVGAD_PRIVATE_KEY;
271
+ if (envKey) {
272
+ if (!/^[a-fA-F0-9]+$/.test(envKey) || envKey.length !== PRIVATE_KEY_HEX_LENGTH) {
273
+ throw new Error(
274
+ `Invalid ENVGAD_PRIVATE_KEY format (expected ${PRIVATE_KEY_HEX_LENGTH}-char hex-encoded PKCS8 DER, got ${envKey.length} chars)`
275
+ );
276
+ }
277
+ return envKey;
278
+ }
279
+ return null;
280
+ }
76
281
 
77
282
  // src/validator.ts
78
- var EnvValidator = class {
283
+ var import_net = __toESM(require("net"), 1);
284
+ var kValidator = /* @__PURE__ */ Symbol.for("dotenv-gad.EnvValidator");
285
+ var EnvValidator = class _EnvValidator {
79
286
  /**
80
287
  * Constructs a new EnvValidator instance.
81
288
  * @param {SchemaDefinition} schema The schema definition for the environment variables.
82
289
  * @param {Object} [options] Optional options for the validation process.
83
290
  * @param {boolean} [options.strict] When true, environment variables not present in the schema will be rejected.
291
+ * @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.
292
+ * @param {string} [options.keysPath] Path to the `.env.keys` file (default: `.env.keys`).
84
293
  */
85
294
  constructor(schema, options) {
86
295
  this.schema = schema;
87
296
  this.options = options;
297
+ Object.defineProperty(this, kValidator, { value: true, enumerable: false });
88
298
  }
299
+ 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])+$/;
300
+ static LOCAL_MAX = 64;
301
+ static DOMAIN_MAX = 255;
302
+ static DOMAIN_PART_MAX = 63;
89
303
  errors = [];
304
+ /** Bundler-safe instanceof — checks the symbol marker, not the class name. */
305
+ static [Symbol.hasInstance](instance) {
306
+ return typeof instance === "object" && instance !== null && Object.prototype.hasOwnProperty.call(instance, kValidator);
307
+ }
90
308
  validate(env) {
91
309
  this.errors = [];
310
+ const { processedEnv, skipKeys } = this.preprocessEncryption(env);
92
311
  const result = {};
93
312
  const groupedEnv = {};
94
313
  const prefixes = [];
@@ -100,14 +319,14 @@ var EnvValidator = class {
100
319
  groupedEnv[k] = {};
101
320
  }
102
321
  }
103
- const envKeys = Object.keys(env);
322
+ const envKeys = Object.keys(processedEnv);
104
323
  for (let i = 0; i < envKeys.length; i++) {
105
324
  const eKey = envKeys[i];
106
325
  for (let j = 0; j < prefixes.length; j++) {
107
326
  const { key, prefix } = prefixes[j];
108
327
  if (eKey.startsWith(prefix)) {
109
328
  const subKey = eKey.slice(prefix.length);
110
- groupedEnv[key][subKey] = env[eKey];
329
+ groupedEnv[key][subKey] = processedEnv[eKey];
111
330
  }
112
331
  }
113
332
  }
@@ -115,9 +334,10 @@ var EnvValidator = class {
115
334
  for (let i = 0; i < schemaKeys.length; i++) {
116
335
  const key = schemaKeys[i];
117
336
  const rule = this.schema[key];
337
+ if (skipKeys.has(key)) continue;
118
338
  try {
119
- const valToValidate = groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 ? groupedEnv[key] : env[key];
120
- if (groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 && env[key] !== void 0) {
339
+ const valToValidate = groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 ? groupedEnv[key] : processedEnv[key];
340
+ if (groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 && processedEnv[key] !== void 0) {
121
341
  console.warn(`Both prefixed variables and top-level ${key} exist; prefixed vars are used`);
122
342
  }
123
343
  if (this.options?.strict && groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0) {
@@ -137,16 +357,16 @@ var EnvValidator = class {
137
357
  } catch (error) {
138
358
  if (error instanceof Error) {
139
359
  let displayedValue;
140
- if (env[key] === void 0) {
360
+ if (processedEnv[key] === void 0) {
141
361
  displayedValue = void 0;
142
362
  } else if (this.options?.includeRaw) {
143
363
  if (rule.sensitive && !this.options?.includeSensitive) {
144
364
  displayedValue = "****";
145
365
  } else {
146
- displayedValue = env[key];
366
+ displayedValue = processedEnv[key];
147
367
  }
148
368
  } else {
149
- displayedValue = this.redactValue(env[key], rule.sensitive);
369
+ displayedValue = this.redactValue(processedEnv[key], rule.sensitive);
150
370
  }
151
371
  this.errors.push({
152
372
  key,
@@ -182,7 +402,17 @@ var EnvValidator = class {
182
402
  }
183
403
  return result;
184
404
  }
185
- // Redact or trim sensitive values for error reporting
405
+ /**
406
+ * Redacts a value to hide its contents, following these rules:
407
+ * - If `value` is undefined, returns undefined.
408
+ * - If `sensitive` is true, returns `"****"`.
409
+ * - If `value` is not a string, returns the original value.
410
+ * - If `value` is a string longer than 64 characters, truncates it to 4 characters
411
+ * at the start and end, and replaces the middle with `"..."`.
412
+ * - Otherwise, returns the original string.
413
+ * @param value The value to redact.
414
+ * @param sensitive If true, redacts the value to `"****"`.
415
+ */
186
416
  redactValue(value, sensitive) {
187
417
  if (value === void 0) return void 0;
188
418
  if (sensitive) return "****";
@@ -192,8 +422,16 @@ var EnvValidator = class {
192
422
  }
193
423
  return value;
194
424
  }
195
- // Try to quickly determine if a string *might* be JSON before parsing to avoid
196
- // costly exceptions in the hot path for clearly non-JSON values.
425
+ /**
426
+ * Tries to parse the given value as a JSON object.
427
+ * Returns `{ ok: true, value: JSON.parse(s) }` if successful,
428
+ * or `{ ok: false }` if not.
429
+ * The following conditions will cause the function to return `{ ok: false }`:
430
+ * - `value` is not a string
431
+ * - `value` is an empty string
432
+ * - `value` does not start with one of the following characters: `{`, `[`, `"`, `t`, `f`, `n`, or a digit
433
+ * - `value` cannot be parsed as a JSON object
434
+ */
197
435
  tryParseJSON(value) {
198
436
  if (typeof value !== "string") return { ok: false };
199
437
  const s = value.trim();
@@ -215,12 +453,20 @@ var EnvValidator = class {
215
453
  throw new Error(`Missing required environment variable`);
216
454
  return effectiveRule.default;
217
455
  }
456
+ if (typeof value === "string") {
457
+ value = value.trim();
458
+ if (value === "") {
459
+ if (effectiveRule.required)
460
+ throw new Error(`Missing required environment variable`);
461
+ return effectiveRule.default;
462
+ }
463
+ }
218
464
  if (effectiveRule.transform) {
219
465
  value = effectiveRule.transform(value);
220
466
  }
221
467
  switch (effectiveRule.type) {
222
468
  case "string":
223
- value = String(value).trim();
469
+ value = String(value);
224
470
  if (effectiveRule.minLength !== void 0 && value.length < effectiveRule.minLength) {
225
471
  throw new Error(
226
472
  `Environment variable ${key} must be at least ${effectiveRule.minLength} characters`
@@ -251,9 +497,9 @@ var EnvValidator = class {
251
497
  case "boolean":
252
498
  if (typeof value === "string") {
253
499
  value = value.toLowerCase();
254
- if (value === "true") {
500
+ if (value === "true" || value === "yes" || value === "1" || value === "on") {
255
501
  value = true;
256
- } else if (value === "false") {
502
+ } else if (value === "false" || value === "no" || value === "0" || value === "off") {
257
503
  value = false;
258
504
  }
259
505
  }
@@ -279,12 +525,12 @@ var EnvValidator = class {
279
525
  }
280
526
  break;
281
527
  case "email":
282
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
528
+ if (!this.validateEmail(value)) {
283
529
  throw new Error("Must be a valid email");
284
530
  }
285
531
  break;
286
532
  case "ip":
287
- if (!/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(String(value))) {
533
+ if (!import_net.default.isIP(value)) {
288
534
  throw new Error("Must be a valid IP address");
289
535
  }
290
536
  break;
@@ -369,12 +615,100 @@ var EnvValidator = class {
369
615
  }
370
616
  return value;
371
617
  }
372
- getEffectiveRule(key, rule) {
618
+ getEffectiveRule(_key, rule) {
373
619
  const envName = process.env.NODE_ENV || "development";
374
620
  const envRule = rule.env?.[envName] || {};
375
621
  return { ...rule, ...envRule };
376
622
  }
623
+ /**
624
+ * Preprocesses environment variables to detect and handle encrypted values.
625
+ * 1. Decrypts any encrypted values using the private key.
626
+ * 2. Checks if any plaintext values are present for fields that should be encrypted.
627
+ * 3. Flags any env value that looks encrypted but the schema doesn't declare encrypted: true.
628
+ * @returns An object containing the processed environment variables and a set of keys that were skipped due to errors.
629
+ */
630
+ preprocessEncryption(env) {
631
+ const processedEnv = { ...env };
632
+ const skipKeys = /* @__PURE__ */ new Set();
633
+ const encryptedSchemaKeys = Object.keys(this.schema).filter(
634
+ (k) => this.schema[k].encrypted
635
+ );
636
+ if (encryptedSchemaKeys.length > 0) {
637
+ const needsDecryption = encryptedSchemaKeys.some(
638
+ (k) => processedEnv[k] != null && processedEnv[k] !== "" && isEncryptedValue(processedEnv[k])
639
+ );
640
+ let privateKeyHex = null;
641
+ if (needsDecryption) {
642
+ privateKeyHex = loadPrivateKey({ keysPath: this.options?.keysPath });
643
+ if (!privateKeyHex) {
644
+ throw new EncryptionKeyMissingError("decryption");
645
+ }
646
+ }
647
+ for (const key of encryptedSchemaKeys) {
648
+ const value = processedEnv[key];
649
+ if (value == null || value === "") continue;
650
+ if (isEncryptedValue(value)) {
651
+ try {
652
+ processedEnv[key] = decryptEnvValue(value, privateKeyHex, key);
653
+ } catch (err) {
654
+ this.errors.push({
655
+ key,
656
+ message: err instanceof Error ? err.message : "Decryption failed",
657
+ rule: this.schema[key]
658
+ });
659
+ skipKeys.add(key);
660
+ }
661
+ } else {
662
+ if (this.options?.allowPlaintext) {
663
+ console.warn(
664
+ `[dotenv-gad] "${key}" has a plaintext value but schema declares encrypted: true. Run: npx dotenv-gad encrypt`
665
+ );
666
+ } else {
667
+ this.errors.push({
668
+ key,
669
+ message: "Must be encrypted. Run: npx dotenv-gad encrypt",
670
+ rule: this.schema[key]
671
+ });
672
+ skipKeys.add(key);
673
+ }
674
+ }
675
+ }
676
+ }
677
+ for (const [eKey, value] of Object.entries(processedEnv)) {
678
+ if (skipKeys.has(eKey)) continue;
679
+ if (value && isEncryptedValue(value) && !this.schema[eKey]?.encrypted) {
680
+ this.errors.push({
681
+ key: eKey,
682
+ message: "Encrypted value found but schema does not declare encrypted: true"
683
+ });
684
+ skipKeys.add(eKey);
685
+ }
686
+ }
687
+ return { processedEnv, skipKeys };
688
+ }
689
+ /*
690
+ * Email validation logic adapted from:
691
+ * Project: email-validator
692
+ * * Repository: https://github.com/manishsaraan/email-validator
693
+ *
694
+ * * Adapted to enforce email validation rules more strictly
695
+ */
696
+ validateEmail(email) {
697
+ if (!email) return false;
698
+ const emailParts = email.split("@");
699
+ if (emailParts.length !== 2) return false;
700
+ const [local, domain] = emailParts;
701
+ if (local.length > _EnvValidator.LOCAL_MAX || domain.length > _EnvValidator.DOMAIN_MAX) return false;
702
+ const domainParts = domain.split(".");
703
+ if (domainParts.some((part) => part.length > _EnvValidator.DOMAIN_PART_MAX)) return false;
704
+ return _EnvValidator.EMAIL_REGEX.test(email);
705
+ }
377
706
  };
707
+ Object.defineProperty(EnvValidator, "name", {
708
+ value: "EnvValidator",
709
+ configurable: true,
710
+ writable: false
711
+ });
378
712
 
379
713
  // src/schema.ts
380
714
  function defineSchema(schema) {
@@ -382,9 +716,19 @@ function defineSchema(schema) {
382
716
  }
383
717
 
384
718
  // src/utils.ts
385
- var import_dotenv = __toESM(require("dotenv"), 1);
719
+ var import_dotenv = require("dotenv");
720
+ var import_node_fs2 = require("fs");
721
+ function readEnvFile(path) {
722
+ const filePath = path ?? ".env";
723
+ if (!(0, import_node_fs2.existsSync)(filePath)) return {};
724
+ try {
725
+ return (0, import_dotenv.parse)((0, import_node_fs2.readFileSync)(filePath, "utf-8"));
726
+ } catch {
727
+ return {};
728
+ }
729
+ }
386
730
  function loadEnv(schema, options) {
387
- const fileEnv = import_dotenv.default.config({ debug: false, path: options?.path }).parsed || {};
731
+ const fileEnv = readEnvFile(options?.path);
388
732
  const env = { ...process.env, ...fileEnv };
389
733
  const validator = new EnvValidator(schema, options);
390
734
  return validator.validate(env);
@@ -415,9 +759,8 @@ function composeSchema(...schemas) {
415
759
  }
416
760
 
417
761
  // src/index.ts
418
- var import_dotenv2 = __toESM(require("dotenv"), 1);
419
762
  function validateEnv(schema, options) {
420
- const fileEnv = import_dotenv2.default.config({ debug: false, path: options?.path }).parsed || {};
763
+ const fileEnv = readEnvFile(options?.path);
421
764
  const env = { ...process.env, ...fileEnv };
422
765
  const validator = new EnvValidator(schema, options);
423
766
  return validator.validate(env);
@@ -425,12 +768,20 @@ function validateEnv(schema, options) {
425
768
  // Annotate the CommonJS export names for ESM import in node:
426
769
  0 && (module.exports = {
427
770
  AggregateError,
771
+ DecryptionFailedError,
772
+ EncryptedFieldMismatchError,
773
+ EncryptionKeyMissingError,
428
774
  EnvAggregateError,
429
775
  EnvValidationError,
430
776
  EnvValidator,
431
777
  composeSchema,
432
778
  createEnvProxy,
779
+ decryptEnvValue,
433
780
  defineSchema,
781
+ encryptEnvValue,
782
+ generateKeyPair,
783
+ isEncryptedValue,
434
784
  loadEnv,
785
+ loadPrivateKey,
435
786
  validateEnv
436
787
  });
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { EnvValidator } from "./validator.js";
2
2
  import { defineSchema } 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
- import dotenv from "dotenv";
6
+ import { generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey, } from "./crypto.js";
7
+ import { readEnvFile } from "./utils.js";
7
8
  export { defineSchema, EnvAggregateError,
8
9
  /** @deprecated Use `EnvAggregateError` instead. */
9
- AggregateError, EnvValidationError, EnvValidator, loadEnv, createEnvProxy, composeSchema, };
10
+ AggregateError, EnvValidationError, EncryptionKeyMissingError, DecryptionFailedError, EncryptedFieldMismatchError, EnvValidator, loadEnv, createEnvProxy, composeSchema, generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey, };
10
11
  export function validateEnv(schema, options) {
11
- const fileEnv = dotenv.config({ debug: false, path: options?.path }).parsed || {};
12
+ const fileEnv = readEnvFile(options?.path);
12
13
  const env = { ...process.env, ...fileEnv };
13
14
  const validator = new EnvValidator(schema, options);
14
15
  return validator.validate(env);
@@ -1,2 +1,2 @@
1
1
  import { Command } from "commander";
2
- export default function (program: Command): Command;
2
+ export default function (_program: Command): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export default function (_program: Command): Command;
@@ -1,2 +1,2 @@
1
1
  import { Command } from "commander";
2
- export default function (program: Command): Command;
2
+ export default function (_program: Command): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export default function (_program: Command): Command;
@@ -1,2 +1,2 @@
1
1
  import { Command } from "commander";
2
- export default function (program: Command): Command;
2
+ export default function (_program: Command): Command;
@@ -1,2 +1,2 @@
1
1
  import { Command } from "commander";
2
- export default function (program: Command): Command;
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;
@@ -1,2 +1,2 @@
1
1
  import { Command } from "commander";
2
- export default function (program: Command): Command;
2
+ export default function (_program: Command): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export default function (_program: Command): Command;