dotenv-gad 1.5.0 → 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 CHANGED
@@ -4,11 +4,12 @@
4
4
  [![CI](https://github.com/kasimlyee/dotenv-gad/actions/workflows/ci.yml/badge.svg)](https://github.com/kasimlyee/dotenv-gad/actions/workflows/ci.yml)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
+ [![Bun](https://img.shields.io/badge/bun-%3E%3D1.0-blue)](https://bun.sh)
7
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-first--class-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
9
  [![Docs](https://img.shields.io/badge/docs-latest-blue?style=flat-square)](https://kasimlyee.github.io/dotenv-gad/)
9
10
  [![npm downloads](https://img.shields.io/npm/dm/dotenv-gad.svg)](https://www.npmjs.com/package/dotenv-gad)
10
11
 
11
- **dotenv-gad** is an environment variable validation library that brings type safety, schema validation, and runtime checks to your Node.js and JavaScript applications. It works with any environment variable source — `.env` files, platform dashboards (Vercel, Railway, Docker), CI/CD pipelines, or `process.env` directly.
12
+ **dotenv-gad** is an environment variable validation library that brings type safety, schema validation, and runtime checks to your Node.js, Bun, and JavaScript applications. It works with any environment variable source — `.env` files, platform dashboards (Vercel, Railway, Docker), CI/CD pipelines, or `process.env` directly.
12
13
 
13
14
  - Type-safe environment variables with full IntelliSense
14
15
  - Schema validation (string, number, boolean, url, email, ip, port, json, array, object)
@@ -28,6 +29,8 @@ npm install dotenv-gad
28
29
  yarn add dotenv-gad
29
30
  # or
30
31
  pnpm add dotenv-gad
32
+ # or
33
+ bun add dotenv-gad
31
34
  ```
32
35
 
33
36
  ## Quick Start
@@ -240,6 +243,36 @@ const env = loadEnv(schema);
240
243
  // { DATABASE: { DB_NAME: 'mydb', PORT: 5432, PWD: 'supersecret' } }
241
244
  ```
242
245
 
246
+ ## Bun Support
247
+
248
+ **dotenv-gad** has support for Bun! All features work seamlessly with Bun's runtime.
249
+
250
+ ### Using the CLI with Bun
251
+
252
+ ```bash
253
+ bunx dotenv-gad check
254
+ bunx dotenv-gad keygen
255
+ bunx dotenv-gad encrypt
256
+ ```
257
+
258
+ ### Bun Runtime Example
259
+
260
+ ```typescript
261
+ import { loadEnv } from "dotenv-gad";
262
+ import schema from "./env.schema";
263
+
264
+ const env = loadEnv(schema);
265
+ console.log(`Server running on port ${env.PORT}`);
266
+
267
+ // Start a Bun HTTP server
268
+ Bun.serve({
269
+ port: env.PORT,
270
+ fetch() {
271
+ return new Response("Hello from Bun with dotenv-gad!");
272
+ },
273
+ });
274
+ ```
275
+
243
276
  ## Framework Integrations
244
277
 
245
278
  ### Express.js
@@ -9,7 +9,8 @@ import { loadSchema } from "./utils.js";
9
9
  export default function (_program) {
10
10
  return new Command("encrypt")
11
11
  .description("Encrypt plaintext values for fields marked encrypted: true in the schema")
12
- .action(async (_opts, command) => {
12
+ .option("--no-backup", "Skip creating a plaintext .env.bak backup file")
13
+ .action(async (opts, command) => {
13
14
  const rootOpts = command.parent.opts();
14
15
  const envPath = rootOpts.env ?? ".env";
15
16
  const schemaPath = rootOpts.schema ?? "env.schema.ts";
@@ -72,10 +73,15 @@ export default function (_program) {
72
73
  }
73
74
  }
74
75
  if (encryptedCount > 0) {
75
- copyFileSync(envPath, `${envPath}.bak`);
76
+ if (opts.backup !== false) {
77
+ copyFileSync(envPath, `${envPath}.bak`);
78
+ }
76
79
  writeFileSync(envPath, updatedContent);
80
+ const backupNote = opts.backup !== false
81
+ ? chalk.yellow(` ⚠ Backup ${envPath}.bak contains plaintext secrets — delete it after verifying.`)
82
+ : chalk.dim(` No backup created (--no-backup).`);
77
83
  console.log(`\n${chalk.green("✓")} ${encryptedCount} field(s) encrypted in ${chalk.bold(envPath)}\n` +
78
- chalk.dim(` Backup saved to ${envPath}.bak`));
84
+ backupNote);
79
85
  }
80
86
  else {
81
87
  console.log(chalk.dim("\nNo fields needed encryption."));
@@ -41,12 +41,18 @@ export async function applyFix(issues, schema, envPath = ".env") {
41
41
  }
42
42
  // Sanitize: strip newlines and carriage returns to prevent .env injection
43
43
  const sanitized = input.replace(/[\r\n]/g, "");
44
+ // Quote the value if it contains characters with special meaning in .env files
45
+ // (# starts a comment, leading/trailing spaces are stripped without quotes)
46
+ const needsQuoting = /[#"\\]/.test(sanitized) || sanitized !== sanitized.trim();
47
+ const envValue = needsQuoting
48
+ ? `"${sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
49
+ : sanitized;
44
50
  const lineIndex = envLines.findIndex((line) => line.startsWith(`${key}=`));
45
51
  if (lineIndex >= 0) {
46
- envLines[lineIndex] = `${key}=${sanitized}`;
52
+ envLines[lineIndex] = `${key}=${envValue}`;
47
53
  }
48
54
  else {
49
- envLines.push(`${key}=${sanitized}`);
55
+ envLines.push(`${key}=${envValue}`);
50
56
  }
51
57
  }
52
58
  }
package/dist/crypto.js CHANGED
@@ -1,6 +1,8 @@
1
- import { createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, } from "node:crypto";
1
+ import { createPrivateKey, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, } from "node:crypto";
2
+ import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { DecryptionFailedError } from "./errors.js";
5
+ import { getEnv } from "./runtime.js";
4
6
  const SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
5
7
  const RAW_KEY_LENGTH = 32;
6
8
  const NONCE_LENGTH = 12;
@@ -54,12 +56,12 @@ export function encryptEnvValue(plaintext, recipientPublicKeyHex, varName) {
54
56
  });
55
57
  const encKey = Buffer.from(hkdfSync("sha256", sharedSecret, HKDF_SALT, HKDF_INFO, 32));
56
58
  const nonce = randomBytes(NONCE_LENGTH);
57
- const cipher = createCipheriv("chacha20-poly1305", encKey, nonce, { authTagLength: AUTH_TAG_LENGTH });
58
- cipher.setAAD(Buffer.from(`${AAD_PREFIX}${varName}`));
59
- const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
60
- const authTag = cipher.getAuthTag();
59
+ const aad = Buffer.from(`${AAD_PREFIX}${varName}`);
60
+ const cipher = chacha20poly1305(encKey, nonce, aad);
61
+ // encrypt() returns ciphertext || 16-byte auth tag concatenated
62
+ const encrypted = cipher.encrypt(Buffer.from(plaintext, "utf8"));
61
63
  const rawEphPubKey = ephSpki.subarray(SPKI_PREFIX.length);
62
- const payload = Buffer.concat([rawEphPubKey, nonce, ciphertext, authTag]);
64
+ const payload = Buffer.concat([rawEphPubKey, nonce, encrypted]);
63
65
  return `${PROTOCOL_PREFIX}${ENCRYPTION_VERSION}:${payload.toString("base64")}`;
64
66
  }
65
67
  /**
@@ -94,9 +96,8 @@ export function decryptEnvValue(token, privateKeyHex, varName) {
94
96
  }
95
97
  const rawEphPubKey = payload.subarray(0, RAW_KEY_LENGTH);
96
98
  const nonce = payload.subarray(RAW_KEY_LENGTH, RAW_KEY_LENGTH + NONCE_LENGTH);
97
- const rest = payload.subarray(RAW_KEY_LENGTH + NONCE_LENGTH);
98
- const authTag = rest.subarray(rest.length - AUTH_TAG_LENGTH);
99
- const ciphertext = rest.subarray(0, rest.length - AUTH_TAG_LENGTH);
99
+ // rest = ciphertext || 16-byte auth tag (as produced by @noble/ciphers encrypt)
100
+ const encrypted = payload.subarray(RAW_KEY_LENGTH + NONCE_LENGTH);
100
101
  const ephSpki = Buffer.concat([SPKI_PREFIX, rawEphPubKey]);
101
102
  // ECDH: recipient private × ephemeral public → shared secret
102
103
  const pkcs8 = Buffer.from(privateKeyHex, "hex");
@@ -106,12 +107,11 @@ export function decryptEnvValue(token, privateKeyHex, varName) {
106
107
  });
107
108
  // HKDF-SHA256: same derivation as encryption
108
109
  const encKey = Buffer.from(hkdfSync("sha256", sharedSecret, HKDF_SALT, HKDF_INFO, 32));
109
- const decipher = createDecipheriv("chacha20-poly1305", encKey, nonce, { authTagLength: AUTH_TAG_LENGTH });
110
- decipher.setAAD(Buffer.from(`${AAD_PREFIX}${varName}`));
111
- decipher.setAuthTag(authTag);
110
+ const aad = Buffer.from(`${AAD_PREFIX}${varName}`);
111
+ const decipher = chacha20poly1305(encKey, nonce, aad);
112
112
  try {
113
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
114
- return plaintext.toString("utf8");
113
+ const plaintext = decipher.decrypt(encrypted);
114
+ return Buffer.from(plaintext).toString("utf8");
115
115
  }
116
116
  catch {
117
117
  throw new DecryptionFailedError(varName);
@@ -151,8 +151,11 @@ export function loadPrivateKey(options = {}) {
151
151
  return key;
152
152
  }
153
153
  }
154
- const envKey = process.env.ENVGAD_PRIVATE_KEY;
154
+ const runtimeEnv = getEnv();
155
+ const envKey = runtimeEnv.ENVGAD_PRIVATE_KEY;
155
156
  if (envKey) {
157
+ // Remove the key from environment after reading for security
158
+ delete runtimeEnv.ENVGAD_PRIVATE_KEY;
156
159
  if (!/^[a-fA-F0-9]+$/.test(envKey) || envKey.length !== PRIVATE_KEY_HEX_LENGTH) {
157
160
  throw new Error(`Invalid ENVGAD_PRIVATE_KEY format (expected ${PRIVATE_KEY_HEX_LENGTH}-char hex-encoded PKCS8 DER, got ${envKey.length} chars)`);
158
161
  }
package/dist/index.cjs CHANGED
@@ -43,6 +43,10 @@ __export(src_exports, {
43
43
  defineSchema: () => defineSchema,
44
44
  encryptEnvValue: () => encryptEnvValue,
45
45
  generateKeyPair: () => generateKeyPair,
46
+ getEnv: () => getEnv,
47
+ getRuntimeName: () => getRuntimeName,
48
+ getRuntimeVersion: () => getRuntimeVersion,
49
+ isBun: () => isBun,
46
50
  isEncryptedValue: () => isEncryptedValue,
47
51
  loadEnv: () => loadEnv,
48
52
  loadPrivateKey: () => loadPrivateKey,
@@ -144,7 +148,30 @@ var EncryptedFieldMismatchError = class _EncryptedFieldMismatchError extends Err
144
148
 
145
149
  // src/crypto.ts
146
150
  var import_node_crypto = require("crypto");
151
+ var import_chacha = require("@noble/ciphers/chacha.js");
147
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
148
175
  var SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
149
176
  var RAW_KEY_LENGTH = 32;
150
177
  var NONCE_LENGTH = 12;
@@ -184,17 +211,11 @@ function encryptEnvValue(plaintext, recipientPublicKeyHex, varName) {
184
211
  (0, import_node_crypto.hkdfSync)("sha256", sharedSecret, HKDF_SALT, HKDF_INFO, 32)
185
212
  );
186
213
  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();
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"));
196
217
  const rawEphPubKey = ephSpki.subarray(SPKI_PREFIX.length);
197
- const payload = Buffer.concat([rawEphPubKey, nonce, ciphertext, authTag]);
218
+ const payload = Buffer.concat([rawEphPubKey, nonce, encrypted]);
198
219
  return `${PROTOCOL_PREFIX}${ENCRYPTION_VERSION}:${payload.toString("base64")}`;
199
220
  }
200
221
  function decryptEnvValue(token, privateKeyHex, varName) {
@@ -222,9 +243,7 @@ function decryptEnvValue(token, privateKeyHex, varName) {
222
243
  }
223
244
  const rawEphPubKey = payload.subarray(0, RAW_KEY_LENGTH);
224
245
  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);
246
+ const encrypted = payload.subarray(RAW_KEY_LENGTH + NONCE_LENGTH);
228
247
  const ephSpki = Buffer.concat([SPKI_PREFIX, rawEphPubKey]);
229
248
  const pkcs8 = Buffer.from(privateKeyHex, "hex");
230
249
  const sharedSecret = (0, import_node_crypto.diffieHellman)({
@@ -234,17 +253,11 @@ function decryptEnvValue(token, privateKeyHex, varName) {
234
253
  const encKey = Buffer.from(
235
254
  (0, import_node_crypto.hkdfSync)("sha256", sharedSecret, HKDF_SALT, HKDF_INFO, 32)
236
255
  );
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);
256
+ const aad = Buffer.from(`${AAD_PREFIX}${varName}`);
257
+ const decipher = (0, import_chacha.chacha20poly1305)(encKey, nonce, aad);
245
258
  try {
246
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
247
- return plaintext.toString("utf8");
259
+ const plaintext = decipher.decrypt(encrypted);
260
+ return Buffer.from(plaintext).toString("utf8");
248
261
  } catch {
249
262
  throw new DecryptionFailedError(varName);
250
263
  }
@@ -267,8 +280,10 @@ function loadPrivateKey(options = {}) {
267
280
  return key;
268
281
  }
269
282
  }
270
- const envKey = process.env.ENVGAD_PRIVATE_KEY;
283
+ const runtimeEnv = getEnv();
284
+ const envKey = runtimeEnv.ENVGAD_PRIVATE_KEY;
271
285
  if (envKey) {
286
+ delete runtimeEnv.ENVGAD_PRIVATE_KEY;
272
287
  if (!/^[a-fA-F0-9]+$/.test(envKey) || envKey.length !== PRIVATE_KEY_HEX_LENGTH) {
273
288
  throw new Error(
274
289
  `Invalid ENVGAD_PRIVATE_KEY format (expected ${PRIVATE_KEY_HEX_LENGTH}-char hex-encoded PKCS8 DER, got ${envKey.length} chars)`
@@ -296,7 +311,7 @@ var EnvValidator = class _EnvValidator {
296
311
  this.options = options;
297
312
  Object.defineProperty(this, kValidator, { value: true, enumerable: false });
298
313
  }
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])+$/;
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])+$/;
300
315
  static LOCAL_MAX = 64;
301
316
  static DOMAIN_MAX = 255;
302
317
  static DOMAIN_PART_MAX = 63;
@@ -616,7 +631,8 @@ var EnvValidator = class _EnvValidator {
616
631
  return value;
617
632
  }
618
633
  getEffectiveRule(_key, rule) {
619
- const envName = process.env.NODE_ENV || "development";
634
+ const runtimeEnv = getEnv();
635
+ const envName = runtimeEnv.NODE_ENV || "development";
620
636
  const envRule = rule.env?.[envName] || {};
621
637
  return { ...rule, ...envRule };
622
638
  }
@@ -722,14 +738,25 @@ function readEnvFile(path) {
722
738
  const filePath = path ?? ".env";
723
739
  if (!(0, import_node_fs2.existsSync)(filePath)) return {};
724
740
  try {
725
- return (0, import_dotenv.parse)((0, import_node_fs2.readFileSync)(filePath, "utf-8"));
726
- } catch {
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}`);
727
753
  return {};
728
754
  }
729
755
  }
730
756
  function loadEnv(schema, options) {
731
757
  const fileEnv = readEnvFile(options?.path);
732
- const env = { ...process.env, ...fileEnv };
758
+ const runtimeEnv = getEnv();
759
+ const env = { ...runtimeEnv, ...fileEnv };
733
760
  const validator = new EnvValidator(schema, options);
734
761
  return validator.validate(env);
735
762
  }
@@ -780,6 +807,10 @@ function validateEnv(schema, options) {
780
807
  defineSchema,
781
808
  encryptEnvValue,
782
809
  generateKeyPair,
810
+ getEnv,
811
+ getRuntimeName,
812
+ getRuntimeVersion,
813
+ isBun,
783
814
  isEncryptedValue,
784
815
  loadEnv,
785
816
  loadPrivateKey,
package/dist/index.js CHANGED
@@ -5,9 +5,10 @@ import { loadEnv, createEnvProxy } from "./utils.js";
5
5
  import { composeSchema } from "./compose.js";
6
6
  import { generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey, } from "./crypto.js";
7
7
  import { readEnvFile } from "./utils.js";
8
+ import { isBun, getEnv, getRuntimeName, getRuntimeVersion } from "./runtime.js";
8
9
  export { defineSchema, EnvAggregateError,
9
10
  /** @deprecated Use `EnvAggregateError` instead. */
10
- AggregateError, EnvValidationError, EncryptionKeyMissingError, DecryptionFailedError, EncryptedFieldMismatchError, EnvValidator, loadEnv, createEnvProxy, composeSchema, generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey, };
11
+ AggregateError, EnvValidationError, EncryptionKeyMissingError, DecryptionFailedError, EncryptedFieldMismatchError, EnvValidator, loadEnv, createEnvProxy, composeSchema, generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey, isBun, getEnv, getRuntimeName, getRuntimeVersion, };
11
12
  export function validateEnv(schema, options) {
12
13
  const fileEnv = readEnvFile(options?.path);
13
14
  const env = { ...process.env, ...fileEnv };
@@ -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
+ }
@@ -4,14 +4,17 @@
4
4
  * Deliberately free of CLI-only dependencies (chalk, inquirer) so it can be
5
5
  * imported by the Vite plugin without pulling in heavy Node packages.
6
6
  *
7
- * esbuild is required only when loading `.ts` schemas and is imported
7
+ * esbuild is required only when loading `.ts` schemas in Node.js and is imported
8
8
  * lazily via `await import("esbuild")` so the module itself has no
9
9
  * top-level dependency on it.
10
+ *
11
+ * In Bun, TypeScript files are loaded directly without transpilation.
10
12
  */
11
13
  import { readFileSync, writeFileSync, unlinkSync, existsSync } from "fs";
12
14
  import { dirname, join, resolve } from "path";
13
15
  import { fileURLToPath, pathToFileURL } from "url";
14
16
  import { randomBytes } from "crypto";
17
+ import { isBun } from "./runtime.js";
15
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
19
  async function importModule(filePath) {
17
20
  const fileUrl = pathToFileURL(filePath).href;
@@ -23,6 +26,11 @@ async function importModule(filePath) {
23
26
  return schema;
24
27
  }
25
28
  async function loadTsModule(tsFilePath) {
29
+ // Bun can import TypeScript files directly without transpilation
30
+ if (isBun()) {
31
+ return await importModule(tsFilePath);
32
+ }
33
+ // Node.js requires transpilation via esbuild
26
34
  const tempFile = join(__dirname, `../temp-schema-${randomBytes(8).toString("hex")}.mjs`);
27
35
  try {
28
36
  const { transformSync } = await import("esbuild");
@@ -32,7 +40,7 @@ async function loadTsModule(tsFilePath) {
32
40
  loader: "ts",
33
41
  target: "esnext",
34
42
  });
35
- writeFileSync(tempFile, code);
43
+ writeFileSync(tempFile, code, { mode: 0o600 });
36
44
  return await importModule(tempFile);
37
45
  }
38
46
  finally {
@@ -45,7 +53,8 @@ async function loadTsModule(tsFilePath) {
45
53
  * Loads a schema from a file.
46
54
  *
47
55
  * Supports `.ts`, `.js`, `.mjs`, `.cjs`, and `.json` formats.
48
- * TypeScript files are transpiled on-the-fly via esbuild (loaded lazily).
56
+ * TypeScript files are loaded natively in Bun, or transpiled on-the-fly
57
+ * via esbuild in Node.js (loaded lazily).
49
58
  */
50
59
  export async function loadSchema(schemaPath) {
51
60
  const absPath = resolve(schemaPath);
@@ -6,9 +6,10 @@ import { composeSchema } from "./compose.js";
6
6
  import { ExtractEnv, InferEnv } from "./types.js";
7
7
  import { generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey } from "./crypto.js";
8
8
  import type { KeyPair } from "./crypto.js";
9
+ import { isBun, getEnv, getRuntimeName, getRuntimeVersion } from "./runtime.js";
9
10
  export { defineSchema, EnvAggregateError,
10
11
  /** @deprecated Use `EnvAggregateError` instead. */
11
- AggregateError, EnvValidationError, EncryptionKeyMissingError, DecryptionFailedError, EncryptedFieldMismatchError, EnvValidator, loadEnv, createEnvProxy, composeSchema, generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey, };
12
+ AggregateError, EnvValidationError, EncryptionKeyMissingError, DecryptionFailedError, EncryptedFieldMismatchError, EnvValidator, loadEnv, createEnvProxy, composeSchema, generateKeyPair, encryptEnvValue, decryptEnvValue, isEncryptedValue, loadPrivateKey, isBun, getEnv, getRuntimeName, getRuntimeVersion, };
12
13
  export type { SchemaDefinition, SchemaRule, ExtractEnv, InferEnv, KeyPair };
13
14
  export declare function validateEnv(schema: SchemaDefinition, options?: {
14
15
  strict?: boolean;
@@ -0,0 +1,29 @@
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 declare function isBun(): boolean;
12
+ /**
13
+ * Gets the appropriate environment object for the current runtime.
14
+ * In Bun, this returns Bun.env, in Node.js it returns process.env.
15
+ * Both are compatible, but Bun.env may have better performance in Bun.
16
+ *
17
+ * @returns The environment variables object
18
+ */
19
+ export declare function getEnv(): Record<string, string | undefined>;
20
+ /**
21
+ * Gets the current runtime name for logging/debugging purposes.
22
+ * @returns "Bun" or "Node.js"
23
+ */
24
+ export declare function getRuntimeName(): string;
25
+ /**
26
+ * Gets the runtime version string.
27
+ * @returns The version of the current runtime
28
+ */
29
+ export declare function getRuntimeVersion(): string;
@@ -4,15 +4,18 @@
4
4
  * Deliberately free of CLI-only dependencies (chalk, inquirer) so it can be
5
5
  * imported by the Vite plugin without pulling in heavy Node packages.
6
6
  *
7
- * esbuild is required only when loading `.ts` schemas and is imported
7
+ * esbuild is required only when loading `.ts` schemas in Node.js and is imported
8
8
  * lazily via `await import("esbuild")` so the module itself has no
9
9
  * top-level dependency on it.
10
+ *
11
+ * In Bun, TypeScript files are loaded directly without transpilation.
10
12
  */
11
13
  import type { SchemaDefinition } from "./schema.js";
12
14
  /**
13
15
  * Loads a schema from a file.
14
16
  *
15
17
  * Supports `.ts`, `.js`, `.mjs`, `.cjs`, and `.json` formats.
16
- * TypeScript files are transpiled on-the-fly via esbuild (loaded lazily).
18
+ * TypeScript files are loaded natively in Bun, or transpiled on-the-fly
19
+ * via esbuild in Node.js (loaded lazily).
17
20
  */
18
21
  export declare function loadSchema(schemaPath: string): Promise<SchemaDefinition>;
@@ -4,6 +4,8 @@ import type { InferEnv } from "./types.js";
4
4
  * Silently reads and parses a .env file without injecting into process.env.
5
5
  * Returns an empty object when the file does not exist (e.g. Vercel, Railway,
6
6
  * Docker — where env vars are already present in process.env).
7
+ *
8
+ * In Bun, this uses Bun's optimized file reading when available.
7
9
  */
8
10
  export declare function readEnvFile(path?: string): Record<string, string>;
9
11
  /**
package/dist/utils.js CHANGED
@@ -1,19 +1,36 @@
1
1
  import { parse as parseEnv } from "dotenv";
2
2
  import { readFileSync, existsSync } from "node:fs";
3
3
  import { EnvValidator } from "./validator.js";
4
+ import { isBun, getEnv } from "./runtime.js";
4
5
  /**
5
6
  * Silently reads and parses a .env file without injecting into process.env.
6
7
  * Returns an empty object when the file does not exist (e.g. Vercel, Railway,
7
8
  * Docker — where env vars are already present in process.env).
9
+ *
10
+ * In Bun, this uses Bun's optimized file reading when available.
8
11
  */
9
12
  export function readEnvFile(path) {
10
13
  const filePath = path ?? ".env";
11
14
  if (!existsSync(filePath))
12
15
  return {};
13
16
  try {
14
- return parseEnv(readFileSync(filePath, "utf-8"));
17
+ // Use Bun's file reading when available for better performance
18
+ let content;
19
+ if (isBun() && typeof globalThis.Bun?.file === 'function') {
20
+ // Bun's synchronous text reading
21
+ const file = globalThis.Bun.file(filePath);
22
+ content = file.text ? (typeof file.text === 'function' ? file.text() : file.text) : readFileSync(filePath, "utf-8");
23
+ // Note: Bun.file().text() is async, so fallback to readFileSync for sync operation
24
+ content = readFileSync(filePath, "utf-8");
25
+ }
26
+ else {
27
+ content = readFileSync(filePath, "utf-8");
28
+ }
29
+ return parseEnv(content);
15
30
  }
16
- catch {
31
+ catch (err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ console.warn(`[dotenv-gad] Failed to parse ${filePath}: ${message}`);
17
34
  return {};
18
35
  }
19
36
  }
@@ -33,7 +50,9 @@ export function readEnvFile(path) {
33
50
  */
34
51
  export function loadEnv(schema, options) {
35
52
  const fileEnv = readEnvFile(options?.path);
36
- const env = { ...process.env, ...fileEnv };
53
+ // Use runtime-aware environment getter (process.env or Bun.env)
54
+ const runtimeEnv = getEnv();
55
+ const env = { ...runtimeEnv, ...fileEnv };
37
56
  const validator = new EnvValidator(schema, options);
38
57
  return validator.validate(env);
39
58
  }
package/dist/validator.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { EnvAggregateError, EncryptionKeyMissingError } from "./errors.js";
2
2
  import { decryptEnvValue, isEncryptedValue, loadPrivateKey } from "./crypto.js";
3
+ import { getEnv } from "./runtime.js";
3
4
  import net from "net";
4
5
  const kValidator = Symbol.for("dotenv-gad.EnvValidator");
5
6
  export class EnvValidator {
6
7
  schema;
7
8
  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 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
10
  static LOCAL_MAX = 64;
10
11
  static DOMAIN_MAX = 255;
11
12
  static DOMAIN_PART_MAX = 63;
@@ -349,7 +350,8 @@ export class EnvValidator {
349
350
  return value;
350
351
  }
351
352
  getEffectiveRule(_key, rule) {
352
- const envName = process.env.NODE_ENV || "development";
353
+ const runtimeEnv = getEnv();
354
+ const envName = runtimeEnv.NODE_ENV || "development";
353
355
  const envRule = rule.env?.[envName] || {};
354
356
  return { ...rule, ...envRule };
355
357
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotenv-gad",
3
- "version": "1.5.0",
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,6 +98,7 @@
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",