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.
Files changed (42) hide show
  1. package/README.md +81 -11
  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 +100 -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 +40 -28
  13. package/dist/cli/commands/verify.js +101 -0
  14. package/dist/cli/index.js +12 -0
  15. package/dist/crypto.js +165 -0
  16. package/dist/errors.js +92 -4
  17. package/dist/index.cjs +408 -26
  18. package/dist/index.js +6 -4
  19. package/dist/runtime.js +43 -0
  20. package/dist/schema-loader.js +12 -3
  21. package/dist/types/cli/commands/check.d.ts +1 -1
  22. package/dist/types/cli/commands/decrypt.d.ts +2 -0
  23. package/dist/types/cli/commands/docs.d.ts +1 -1
  24. package/dist/types/cli/commands/encrypt.d.ts +2 -0
  25. package/dist/types/cli/commands/fix.d.ts +1 -1
  26. package/dist/types/cli/commands/init.d.ts +1 -1
  27. package/dist/types/cli/commands/keygen.d.ts +2 -0
  28. package/dist/types/cli/commands/rotate.d.ts +2 -0
  29. package/dist/types/cli/commands/status.d.ts +2 -0
  30. package/dist/types/cli/commands/sync.d.ts +1 -1
  31. package/dist/types/cli/commands/verify.d.ts +2 -0
  32. package/dist/types/crypto.d.ts +58 -0
  33. package/dist/types/errors.d.ts +35 -12
  34. package/dist/types/index.d.ts +8 -3
  35. package/dist/types/runtime.d.ts +29 -0
  36. package/dist/types/schema-loader.d.ts +5 -2
  37. package/dist/types/schema.d.ts +7 -0
  38. package/dist/types/utils.d.ts +21 -10
  39. package/dist/types/validator.d.ts +40 -0
  40. package/dist/utils.js +50 -13
  41. package/dist/validator.js +164 -19
  42. package/package.json +6 -4
package/dist/index.cjs CHANGED
@@ -31,18 +31,32 @@ 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
+ getEnv: () => getEnv,
47
+ getRuntimeName: () => getRuntimeName,
48
+ getRuntimeVersion: () => getRuntimeVersion,
49
+ isBun: () => isBun,
50
+ isEncryptedValue: () => isEncryptedValue,
40
51
  loadEnv: () => loadEnv,
52
+ loadPrivateKey: () => loadPrivateKey,
41
53
  validateEnv: () => validateEnv
42
54
  });
43
55
  module.exports = __toCommonJS(src_exports);
44
56
 
45
57
  // src/errors.ts
58
+ var kValidationError = /* @__PURE__ */ Symbol.for("dotenv-gad.EnvValidationError");
59
+ var kAggregateError = /* @__PURE__ */ Symbol.for("dotenv-gad.EnvAggregateError");
46
60
  var EnvValidationError = class extends Error {
47
61
  constructor(key, message, receiveValue) {
48
62
  super(message);
@@ -50,15 +64,40 @@ var EnvValidationError = class extends Error {
50
64
  this.message = message;
51
65
  this.receiveValue = receiveValue;
52
66
  this.name = "EnvValidationError";
67
+ Object.defineProperty(this, kValidationError, { value: true, enumerable: false });
68
+ }
69
+ static [Symbol.hasInstance](instance) {
70
+ return typeof instance === "object" && instance !== null && Object.prototype.hasOwnProperty.call(instance, kValidationError);
53
71
  }
54
72
  };
73
+ Object.defineProperty(EnvValidationError, "name", {
74
+ value: "EnvValidationError",
75
+ configurable: true,
76
+ writable: false
77
+ });
78
+ var errorsMap = /* @__PURE__ */ new WeakMap();
55
79
  var EnvAggregateError = class _EnvAggregateError extends Error {
80
+ get errors() {
81
+ return errorsMap.get(this);
82
+ }
56
83
  constructor(errors, message) {
57
- super(message);
58
- this.errors = errors;
84
+ const summary = errors.map((e) => {
85
+ let line = `
86
+ - ${e.key}: ${e.message}`;
87
+ if (e.value !== void 0) line += ` (received: ${JSON.stringify(e.value)})`;
88
+ if (e.rule?.docs) line += `
89
+ hint: ${e.rule.docs}`;
90
+ return line;
91
+ }).join("");
92
+ super(message + summary);
59
93
  this.name = "EnvAggregateError";
94
+ errorsMap.set(this, errors);
95
+ Object.defineProperty(this, kAggregateError, { value: true, enumerable: false });
60
96
  Object.setPrototypeOf(this, _EnvAggregateError.prototype);
61
97
  }
98
+ static [Symbol.hasInstance](instance) {
99
+ return typeof instance === "object" && instance !== null && Object.prototype.hasOwnProperty.call(instance, kAggregateError);
100
+ }
62
101
  toString() {
63
102
  const errorList = this.errors.map((e) => {
64
103
  let msg = ` - ${e.key}: ${e.message}`;
@@ -68,27 +107,222 @@ var EnvAggregateError = class _EnvAggregateError extends Error {
68
107
  ${e.rule.docs}`;
69
108
  return msg;
70
109
  }).join("\n");
71
- return `${this.message}:
110
+ return `${this.name}: ${this.message}
72
111
  ${errorList}`;
73
112
  }
74
113
  };
114
+ Object.defineProperty(EnvAggregateError, "name", {
115
+ value: "EnvAggregateError",
116
+ configurable: true,
117
+ writable: false
118
+ });
75
119
  var AggregateError = EnvAggregateError;
120
+ var EncryptionKeyMissingError = class _EncryptionKeyMissingError extends Error {
121
+ constructor(context) {
122
+ 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.";
123
+ super(message);
124
+ this.name = "EncryptionKeyMissingError";
125
+ Object.setPrototypeOf(this, _EncryptionKeyMissingError.prototype);
126
+ }
127
+ };
128
+ var DecryptionFailedError = class _DecryptionFailedError extends Error {
129
+ constructor(varName) {
130
+ super(
131
+ `Decryption failed for "${varName}". Possible causes:
132
+ - Wrong private key
133
+ - Corrupted ciphertext
134
+ - Ciphertext moved from a different variable (AAD mismatch)`
135
+ );
136
+ this.name = "DecryptionFailedError";
137
+ Object.setPrototypeOf(this, _DecryptionFailedError.prototype);
138
+ }
139
+ };
140
+ var EncryptedFieldMismatchError = class _EncryptedFieldMismatchError extends Error {
141
+ constructor(varName, shouldBeEncrypted) {
142
+ 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`;
143
+ super(message);
144
+ this.name = "EncryptedFieldMismatchError";
145
+ Object.setPrototypeOf(this, _EncryptedFieldMismatchError.prototype);
146
+ }
147
+ };
148
+
149
+ // src/crypto.ts
150
+ var import_node_crypto = require("crypto");
151
+ var import_chacha = require("@noble/ciphers/chacha.js");
152
+ var import_node_fs = require("fs");
153
+
154
+ // src/runtime.ts
155
+ function isBun() {
156
+ return typeof globalThis.Bun !== "undefined";
157
+ }
158
+ function getEnv() {
159
+ if (isBun() && typeof globalThis.Bun.env !== "undefined") {
160
+ return globalThis.Bun.env;
161
+ }
162
+ return process.env;
163
+ }
164
+ function getRuntimeName() {
165
+ return isBun() ? "Bun" : "Node.js";
166
+ }
167
+ function getRuntimeVersion() {
168
+ if (isBun()) {
169
+ return globalThis.Bun.version || "unknown";
170
+ }
171
+ return process.version;
172
+ }
173
+
174
+ // src/crypto.ts
175
+ var SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
176
+ var RAW_KEY_LENGTH = 32;
177
+ var NONCE_LENGTH = 12;
178
+ var AUTH_TAG_LENGTH = 16;
179
+ var PROTOCOL_PREFIX = "encrypted:";
180
+ var ENCRYPTION_VERSION = "v1";
181
+ var HKDF_INFO = Buffer.from("dotenv-gad:v1");
182
+ var HKDF_SALT = Buffer.alloc(0);
183
+ var AAD_PREFIX = "dotenv-gad:v1:";
184
+ var PRIVATE_KEY_HEX_LENGTH = 96;
185
+ var PUBLIC_KEY_HEX_LENGTH = 88;
186
+ function generateKeyPair() {
187
+ const { publicKey, privateKey } = (0, import_node_crypto.generateKeyPairSync)("x25519", {
188
+ publicKeyEncoding: { type: "spki", format: "der" },
189
+ privateKeyEncoding: { type: "pkcs8", format: "der" }
190
+ });
191
+ return {
192
+ publicKeyHex: publicKey.toString("hex"),
193
+ privateKeyHex: privateKey.toString("hex")
194
+ };
195
+ }
196
+ function encryptEnvValue(plaintext, recipientPublicKeyHex, varName) {
197
+ if (!/^[a-fA-F0-9]+$/.test(recipientPublicKeyHex) || recipientPublicKeyHex.length !== PUBLIC_KEY_HEX_LENGTH) {
198
+ throw new Error(
199
+ `Invalid ENVGAD_PUBLIC_KEY format (expected ${PUBLIC_KEY_HEX_LENGTH}-char hex-encoded SPKI DER, got ${recipientPublicKeyHex.length} chars)`
200
+ );
201
+ }
202
+ const recipientSpki = Buffer.from(recipientPublicKeyHex, "hex");
203
+ const { publicKeyHex: ephPubHex, privateKeyHex: ephPrivHex } = generateKeyPair();
204
+ const ephSpki = Buffer.from(ephPubHex, "hex");
205
+ const ephPkcs8 = Buffer.from(ephPrivHex, "hex");
206
+ const sharedSecret = (0, import_node_crypto.diffieHellman)({
207
+ privateKey: (0, import_node_crypto.createPrivateKey)({ key: ephPkcs8, format: "der", type: "pkcs8" }),
208
+ publicKey: (0, import_node_crypto.createPublicKey)({ key: recipientSpki, format: "der", type: "spki" })
209
+ });
210
+ const encKey = Buffer.from(
211
+ (0, import_node_crypto.hkdfSync)("sha256", sharedSecret, HKDF_SALT, HKDF_INFO, 32)
212
+ );
213
+ const nonce = (0, import_node_crypto.randomBytes)(NONCE_LENGTH);
214
+ const aad = Buffer.from(`${AAD_PREFIX}${varName}`);
215
+ const cipher = (0, import_chacha.chacha20poly1305)(encKey, nonce, aad);
216
+ const encrypted = cipher.encrypt(Buffer.from(plaintext, "utf8"));
217
+ const rawEphPubKey = ephSpki.subarray(SPKI_PREFIX.length);
218
+ const payload = Buffer.concat([rawEphPubKey, nonce, encrypted]);
219
+ return `${PROTOCOL_PREFIX}${ENCRYPTION_VERSION}:${payload.toString("base64")}`;
220
+ }
221
+ function decryptEnvValue(token, privateKeyHex, varName) {
222
+ const prefix = `${PROTOCOL_PREFIX}${ENCRYPTION_VERSION}:`;
223
+ if (!token.startsWith(prefix)) {
224
+ const versionMatch = token.match(/^encrypted:(\w+):/);
225
+ if (versionMatch) {
226
+ throw new Error(
227
+ `Unsupported encryption version: ${versionMatch[1]}. This version of dotenv-gad supports: ${ENCRYPTION_VERSION}`
228
+ );
229
+ }
230
+ throw new Error("Invalid encrypted value format");
231
+ }
232
+ let payload;
233
+ try {
234
+ payload = Buffer.from(token.slice(prefix.length), "base64");
235
+ } catch {
236
+ throw new Error("Invalid base64 encoding in encrypted value");
237
+ }
238
+ const minLength = RAW_KEY_LENGTH + NONCE_LENGTH + AUTH_TAG_LENGTH;
239
+ if (payload.length < minLength) {
240
+ throw new Error(
241
+ `Encrypted value too short: ${payload.length} bytes (minimum: ${minLength})`
242
+ );
243
+ }
244
+ const rawEphPubKey = payload.subarray(0, RAW_KEY_LENGTH);
245
+ const nonce = payload.subarray(RAW_KEY_LENGTH, RAW_KEY_LENGTH + NONCE_LENGTH);
246
+ const encrypted = payload.subarray(RAW_KEY_LENGTH + NONCE_LENGTH);
247
+ const ephSpki = Buffer.concat([SPKI_PREFIX, rawEphPubKey]);
248
+ const pkcs8 = Buffer.from(privateKeyHex, "hex");
249
+ const sharedSecret = (0, import_node_crypto.diffieHellman)({
250
+ privateKey: (0, import_node_crypto.createPrivateKey)({ key: pkcs8, format: "der", type: "pkcs8" }),
251
+ publicKey: (0, import_node_crypto.createPublicKey)({ key: ephSpki, format: "der", type: "spki" })
252
+ });
253
+ const encKey = Buffer.from(
254
+ (0, import_node_crypto.hkdfSync)("sha256", sharedSecret, HKDF_SALT, HKDF_INFO, 32)
255
+ );
256
+ const aad = Buffer.from(`${AAD_PREFIX}${varName}`);
257
+ const decipher = (0, import_chacha.chacha20poly1305)(encKey, nonce, aad);
258
+ try {
259
+ const plaintext = decipher.decrypt(encrypted);
260
+ return Buffer.from(plaintext).toString("utf8");
261
+ } catch {
262
+ throw new DecryptionFailedError(varName);
263
+ }
264
+ }
265
+ function isEncryptedValue(value) {
266
+ return value.startsWith(`${PROTOCOL_PREFIX}${ENCRYPTION_VERSION}:`);
267
+ }
268
+ function loadPrivateKey(options = {}) {
269
+ const keysPath = options.keysPath ?? ".env.keys";
270
+ if ((0, import_node_fs.existsSync)(keysPath)) {
271
+ const content = (0, import_node_fs.readFileSync)(keysPath, "utf8");
272
+ const match = content.match(/^ENVGAD_PRIVATE_KEY=([a-fA-F0-9]+)/m);
273
+ if (match) {
274
+ const key = match[1];
275
+ if (key.length !== PRIVATE_KEY_HEX_LENGTH) {
276
+ throw new Error(
277
+ `Invalid ENVGAD_PRIVATE_KEY in ${keysPath}: expected ${PRIVATE_KEY_HEX_LENGTH}-char hex-encoded PKCS8 DER, got ${key.length} chars`
278
+ );
279
+ }
280
+ return key;
281
+ }
282
+ }
283
+ const runtimeEnv = getEnv();
284
+ const envKey = runtimeEnv.ENVGAD_PRIVATE_KEY;
285
+ if (envKey) {
286
+ delete runtimeEnv.ENVGAD_PRIVATE_KEY;
287
+ if (!/^[a-fA-F0-9]+$/.test(envKey) || envKey.length !== PRIVATE_KEY_HEX_LENGTH) {
288
+ throw new Error(
289
+ `Invalid ENVGAD_PRIVATE_KEY format (expected ${PRIVATE_KEY_HEX_LENGTH}-char hex-encoded PKCS8 DER, got ${envKey.length} chars)`
290
+ );
291
+ }
292
+ return envKey;
293
+ }
294
+ return null;
295
+ }
76
296
 
77
297
  // src/validator.ts
78
- var EnvValidator = class {
298
+ var import_net = __toESM(require("net"), 1);
299
+ var kValidator = /* @__PURE__ */ Symbol.for("dotenv-gad.EnvValidator");
300
+ var EnvValidator = class _EnvValidator {
79
301
  /**
80
302
  * Constructs a new EnvValidator instance.
81
303
  * @param {SchemaDefinition} schema The schema definition for the environment variables.
82
304
  * @param {Object} [options] Optional options for the validation process.
83
305
  * @param {boolean} [options.strict] When true, environment variables not present in the schema will be rejected.
306
+ * @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.
307
+ * @param {string} [options.keysPath] Path to the `.env.keys` file (default: `.env.keys`).
84
308
  */
85
309
  constructor(schema, options) {
86
310
  this.schema = schema;
87
311
  this.options = options;
312
+ Object.defineProperty(this, kValidator, { value: true, enumerable: false });
88
313
  }
314
+ 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])+$/;
315
+ static LOCAL_MAX = 64;
316
+ static DOMAIN_MAX = 255;
317
+ static DOMAIN_PART_MAX = 63;
89
318
  errors = [];
319
+ /** Bundler-safe instanceof — checks the symbol marker, not the class name. */
320
+ static [Symbol.hasInstance](instance) {
321
+ return typeof instance === "object" && instance !== null && Object.prototype.hasOwnProperty.call(instance, kValidator);
322
+ }
90
323
  validate(env) {
91
324
  this.errors = [];
325
+ const { processedEnv, skipKeys } = this.preprocessEncryption(env);
92
326
  const result = {};
93
327
  const groupedEnv = {};
94
328
  const prefixes = [];
@@ -100,14 +334,14 @@ var EnvValidator = class {
100
334
  groupedEnv[k] = {};
101
335
  }
102
336
  }
103
- const envKeys = Object.keys(env);
337
+ const envKeys = Object.keys(processedEnv);
104
338
  for (let i = 0; i < envKeys.length; i++) {
105
339
  const eKey = envKeys[i];
106
340
  for (let j = 0; j < prefixes.length; j++) {
107
341
  const { key, prefix } = prefixes[j];
108
342
  if (eKey.startsWith(prefix)) {
109
343
  const subKey = eKey.slice(prefix.length);
110
- groupedEnv[key][subKey] = env[eKey];
344
+ groupedEnv[key][subKey] = processedEnv[eKey];
111
345
  }
112
346
  }
113
347
  }
@@ -115,9 +349,10 @@ var EnvValidator = class {
115
349
  for (let i = 0; i < schemaKeys.length; i++) {
116
350
  const key = schemaKeys[i];
117
351
  const rule = this.schema[key];
352
+ if (skipKeys.has(key)) continue;
118
353
  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) {
354
+ const valToValidate = groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 ? groupedEnv[key] : processedEnv[key];
355
+ if (groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0 && processedEnv[key] !== void 0) {
121
356
  console.warn(`Both prefixed variables and top-level ${key} exist; prefixed vars are used`);
122
357
  }
123
358
  if (this.options?.strict && groupedEnv[key] && Object.keys(groupedEnv[key]).length > 0) {
@@ -137,16 +372,16 @@ var EnvValidator = class {
137
372
  } catch (error) {
138
373
  if (error instanceof Error) {
139
374
  let displayedValue;
140
- if (env[key] === void 0) {
375
+ if (processedEnv[key] === void 0) {
141
376
  displayedValue = void 0;
142
377
  } else if (this.options?.includeRaw) {
143
378
  if (rule.sensitive && !this.options?.includeSensitive) {
144
379
  displayedValue = "****";
145
380
  } else {
146
- displayedValue = env[key];
381
+ displayedValue = processedEnv[key];
147
382
  }
148
383
  } else {
149
- displayedValue = this.redactValue(env[key], rule.sensitive);
384
+ displayedValue = this.redactValue(processedEnv[key], rule.sensitive);
150
385
  }
151
386
  this.errors.push({
152
387
  key,
@@ -182,7 +417,17 @@ var EnvValidator = class {
182
417
  }
183
418
  return result;
184
419
  }
185
- // Redact or trim sensitive values for error reporting
420
+ /**
421
+ * Redacts a value to hide its contents, following these rules:
422
+ * - If `value` is undefined, returns undefined.
423
+ * - If `sensitive` is true, returns `"****"`.
424
+ * - If `value` is not a string, returns the original value.
425
+ * - If `value` is a string longer than 64 characters, truncates it to 4 characters
426
+ * at the start and end, and replaces the middle with `"..."`.
427
+ * - Otherwise, returns the original string.
428
+ * @param value The value to redact.
429
+ * @param sensitive If true, redacts the value to `"****"`.
430
+ */
186
431
  redactValue(value, sensitive) {
187
432
  if (value === void 0) return void 0;
188
433
  if (sensitive) return "****";
@@ -192,8 +437,16 @@ var EnvValidator = class {
192
437
  }
193
438
  return value;
194
439
  }
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.
440
+ /**
441
+ * Tries to parse the given value as a JSON object.
442
+ * Returns `{ ok: true, value: JSON.parse(s) }` if successful,
443
+ * or `{ ok: false }` if not.
444
+ * The following conditions will cause the function to return `{ ok: false }`:
445
+ * - `value` is not a string
446
+ * - `value` is an empty string
447
+ * - `value` does not start with one of the following characters: `{`, `[`, `"`, `t`, `f`, `n`, or a digit
448
+ * - `value` cannot be parsed as a JSON object
449
+ */
197
450
  tryParseJSON(value) {
198
451
  if (typeof value !== "string") return { ok: false };
199
452
  const s = value.trim();
@@ -215,12 +468,20 @@ var EnvValidator = class {
215
468
  throw new Error(`Missing required environment variable`);
216
469
  return effectiveRule.default;
217
470
  }
471
+ if (typeof value === "string") {
472
+ value = value.trim();
473
+ if (value === "") {
474
+ if (effectiveRule.required)
475
+ throw new Error(`Missing required environment variable`);
476
+ return effectiveRule.default;
477
+ }
478
+ }
218
479
  if (effectiveRule.transform) {
219
480
  value = effectiveRule.transform(value);
220
481
  }
221
482
  switch (effectiveRule.type) {
222
483
  case "string":
223
- value = String(value).trim();
484
+ value = String(value);
224
485
  if (effectiveRule.minLength !== void 0 && value.length < effectiveRule.minLength) {
225
486
  throw new Error(
226
487
  `Environment variable ${key} must be at least ${effectiveRule.minLength} characters`
@@ -251,9 +512,9 @@ var EnvValidator = class {
251
512
  case "boolean":
252
513
  if (typeof value === "string") {
253
514
  value = value.toLowerCase();
254
- if (value === "true") {
515
+ if (value === "true" || value === "yes" || value === "1" || value === "on") {
255
516
  value = true;
256
- } else if (value === "false") {
517
+ } else if (value === "false" || value === "no" || value === "0" || value === "off") {
257
518
  value = false;
258
519
  }
259
520
  }
@@ -279,12 +540,12 @@ var EnvValidator = class {
279
540
  }
280
541
  break;
281
542
  case "email":
282
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
543
+ if (!this.validateEmail(value)) {
283
544
  throw new Error("Must be a valid email");
284
545
  }
285
546
  break;
286
547
  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))) {
548
+ if (!import_net.default.isIP(value)) {
288
549
  throw new Error("Must be a valid IP address");
289
550
  }
290
551
  break;
@@ -369,12 +630,101 @@ var EnvValidator = class {
369
630
  }
370
631
  return value;
371
632
  }
372
- getEffectiveRule(key, rule) {
373
- const envName = process.env.NODE_ENV || "development";
633
+ getEffectiveRule(_key, rule) {
634
+ const runtimeEnv = getEnv();
635
+ const envName = runtimeEnv.NODE_ENV || "development";
374
636
  const envRule = rule.env?.[envName] || {};
375
637
  return { ...rule, ...envRule };
376
638
  }
639
+ /**
640
+ * Preprocesses environment variables to detect and handle encrypted values.
641
+ * 1. Decrypts any encrypted values using the private key.
642
+ * 2. Checks if any plaintext values are present for fields that should be encrypted.
643
+ * 3. Flags any env value that looks encrypted but the schema doesn't declare encrypted: true.
644
+ * @returns An object containing the processed environment variables and a set of keys that were skipped due to errors.
645
+ */
646
+ preprocessEncryption(env) {
647
+ const processedEnv = { ...env };
648
+ const skipKeys = /* @__PURE__ */ new Set();
649
+ const encryptedSchemaKeys = Object.keys(this.schema).filter(
650
+ (k) => this.schema[k].encrypted
651
+ );
652
+ if (encryptedSchemaKeys.length > 0) {
653
+ const needsDecryption = encryptedSchemaKeys.some(
654
+ (k) => processedEnv[k] != null && processedEnv[k] !== "" && isEncryptedValue(processedEnv[k])
655
+ );
656
+ let privateKeyHex = null;
657
+ if (needsDecryption) {
658
+ privateKeyHex = loadPrivateKey({ keysPath: this.options?.keysPath });
659
+ if (!privateKeyHex) {
660
+ throw new EncryptionKeyMissingError("decryption");
661
+ }
662
+ }
663
+ for (const key of encryptedSchemaKeys) {
664
+ const value = processedEnv[key];
665
+ if (value == null || value === "") continue;
666
+ if (isEncryptedValue(value)) {
667
+ try {
668
+ processedEnv[key] = decryptEnvValue(value, privateKeyHex, key);
669
+ } catch (err) {
670
+ this.errors.push({
671
+ key,
672
+ message: err instanceof Error ? err.message : "Decryption failed",
673
+ rule: this.schema[key]
674
+ });
675
+ skipKeys.add(key);
676
+ }
677
+ } else {
678
+ if (this.options?.allowPlaintext) {
679
+ console.warn(
680
+ `[dotenv-gad] "${key}" has a plaintext value but schema declares encrypted: true. Run: npx dotenv-gad encrypt`
681
+ );
682
+ } else {
683
+ this.errors.push({
684
+ key,
685
+ message: "Must be encrypted. Run: npx dotenv-gad encrypt",
686
+ rule: this.schema[key]
687
+ });
688
+ skipKeys.add(key);
689
+ }
690
+ }
691
+ }
692
+ }
693
+ for (const [eKey, value] of Object.entries(processedEnv)) {
694
+ if (skipKeys.has(eKey)) continue;
695
+ if (value && isEncryptedValue(value) && !this.schema[eKey]?.encrypted) {
696
+ this.errors.push({
697
+ key: eKey,
698
+ message: "Encrypted value found but schema does not declare encrypted: true"
699
+ });
700
+ skipKeys.add(eKey);
701
+ }
702
+ }
703
+ return { processedEnv, skipKeys };
704
+ }
705
+ /*
706
+ * Email validation logic adapted from:
707
+ * Project: email-validator
708
+ * * Repository: https://github.com/manishsaraan/email-validator
709
+ *
710
+ * * Adapted to enforce email validation rules more strictly
711
+ */
712
+ validateEmail(email) {
713
+ if (!email) return false;
714
+ const emailParts = email.split("@");
715
+ if (emailParts.length !== 2) return false;
716
+ const [local, domain] = emailParts;
717
+ if (local.length > _EnvValidator.LOCAL_MAX || domain.length > _EnvValidator.DOMAIN_MAX) return false;
718
+ const domainParts = domain.split(".");
719
+ if (domainParts.some((part) => part.length > _EnvValidator.DOMAIN_PART_MAX)) return false;
720
+ return _EnvValidator.EMAIL_REGEX.test(email);
721
+ }
377
722
  };
723
+ Object.defineProperty(EnvValidator, "name", {
724
+ value: "EnvValidator",
725
+ configurable: true,
726
+ writable: false
727
+ });
378
728
 
379
729
  // src/schema.ts
380
730
  function defineSchema(schema) {
@@ -382,10 +732,31 @@ function defineSchema(schema) {
382
732
  }
383
733
 
384
734
  // src/utils.ts
385
- var import_dotenv = __toESM(require("dotenv"), 1);
735
+ var import_dotenv = require("dotenv");
736
+ var import_node_fs2 = require("fs");
737
+ function readEnvFile(path) {
738
+ const filePath = path ?? ".env";
739
+ if (!(0, import_node_fs2.existsSync)(filePath)) return {};
740
+ try {
741
+ let content;
742
+ if (isBun() && typeof globalThis.Bun?.file === "function") {
743
+ const file = globalThis.Bun.file(filePath);
744
+ content = file.text ? typeof file.text === "function" ? file.text() : file.text : (0, import_node_fs2.readFileSync)(filePath, "utf-8");
745
+ content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
746
+ } else {
747
+ content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
748
+ }
749
+ return (0, import_dotenv.parse)(content);
750
+ } catch (err) {
751
+ const message = err instanceof Error ? err.message : String(err);
752
+ console.warn(`[dotenv-gad] Failed to parse ${filePath}: ${message}`);
753
+ return {};
754
+ }
755
+ }
386
756
  function loadEnv(schema, options) {
387
- const fileEnv = import_dotenv.default.config({ debug: false, path: options?.path }).parsed || {};
388
- const env = { ...process.env, ...fileEnv };
757
+ const fileEnv = readEnvFile(options?.path);
758
+ const runtimeEnv = getEnv();
759
+ const env = { ...runtimeEnv, ...fileEnv };
389
760
  const validator = new EnvValidator(schema, options);
390
761
  return validator.validate(env);
391
762
  }
@@ -415,9 +786,8 @@ function composeSchema(...schemas) {
415
786
  }
416
787
 
417
788
  // src/index.ts
418
- var import_dotenv2 = __toESM(require("dotenv"), 1);
419
789
  function validateEnv(schema, options) {
420
- const fileEnv = import_dotenv2.default.config({ debug: false, path: options?.path }).parsed || {};
790
+ const fileEnv = readEnvFile(options?.path);
421
791
  const env = { ...process.env, ...fileEnv };
422
792
  const validator = new EnvValidator(schema, options);
423
793
  return validator.validate(env);
@@ -425,12 +795,24 @@ function validateEnv(schema, options) {
425
795
  // Annotate the CommonJS export names for ESM import in node:
426
796
  0 && (module.exports = {
427
797
  AggregateError,
798
+ DecryptionFailedError,
799
+ EncryptedFieldMismatchError,
800
+ EncryptionKeyMissingError,
428
801
  EnvAggregateError,
429
802
  EnvValidationError,
430
803
  EnvValidator,
431
804
  composeSchema,
432
805
  createEnvProxy,
806
+ decryptEnvValue,
433
807
  defineSchema,
808
+ encryptEnvValue,
809
+ generateKeyPair,
810
+ getEnv,
811
+ getRuntimeName,
812
+ getRuntimeVersion,
813
+ isBun,
814
+ isEncryptedValue,
434
815
  loadEnv,
816
+ loadPrivateKey,
435
817
  validateEnv
436
818
  });
package/dist/index.js CHANGED
@@ -1,14 +1,16 @@
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";
8
+ import { isBun, getEnv, getRuntimeName, getRuntimeVersion } from "./runtime.js";
7
9
  export { defineSchema, EnvAggregateError,
8
10
  /** @deprecated Use `EnvAggregateError` instead. */
9
- AggregateError, EnvValidationError, EnvValidator, loadEnv, createEnvProxy, composeSchema, };
11
+ AggregateError, EnvValidationError, EncryptionKeyMissingError, DecryptionFailedError, EncryptedFieldMismatchError, EnvValidator, loadEnv, createEnvProxy, composeSchema, generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey, isBun, getEnv, getRuntimeName, getRuntimeVersion, };
10
12
  export function validateEnv(schema, options) {
11
- const fileEnv = dotenv.config({ debug: false, path: options?.path }).parsed || {};
13
+ const fileEnv = readEnvFile(options?.path);
12
14
  const env = { ...process.env, ...fileEnv };
13
15
  const validator = new EnvValidator(schema, options);
14
16
  return validator.validate(env);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Runtime detection and compatibility utilities for Node.js and Bun.
3
+ *
4
+ * This module provides utilities to detect the current runtime environment
5
+ * and offers optimized implementations based on the runtime.
6
+ */
7
+ /**
8
+ * Checks if the current runtime is Bun.
9
+ * @returns true if running in Bun, false otherwise
10
+ */
11
+ export function isBun() {
12
+ return typeof globalThis.Bun !== 'undefined';
13
+ }
14
+ /**
15
+ * Gets the appropriate environment object for the current runtime.
16
+ * In Bun, this returns Bun.env, in Node.js it returns process.env.
17
+ * Both are compatible, but Bun.env may have better performance in Bun.
18
+ *
19
+ * @returns The environment variables object
20
+ */
21
+ export function getEnv() {
22
+ if (isBun() && typeof globalThis.Bun.env !== 'undefined') {
23
+ return globalThis.Bun.env;
24
+ }
25
+ return process.env;
26
+ }
27
+ /**
28
+ * Gets the current runtime name for logging/debugging purposes.
29
+ * @returns "Bun" or "Node.js"
30
+ */
31
+ export function getRuntimeName() {
32
+ return isBun() ? 'Bun' : 'Node.js';
33
+ }
34
+ /**
35
+ * Gets the runtime version string.
36
+ * @returns The version of the current runtime
37
+ */
38
+ export function getRuntimeVersion() {
39
+ if (isBun()) {
40
+ return globalThis.Bun.version || 'unknown';
41
+ }
42
+ return process.version;
43
+ }