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 +34 -1
- package/dist/cli/commands/encrypt.js +9 -3
- package/dist/cli/commands/utils.js +8 -2
- package/dist/crypto.js +18 -15
- package/dist/index.cjs +60 -29
- package/dist/index.js +2 -1
- package/dist/runtime.js +43 -0
- package/dist/schema-loader.js +12 -3
- package/dist/types/index.d.ts +2 -1
- package/dist/types/runtime.d.ts +29 -0
- package/dist/types/schema-loader.d.ts +5 -2
- package/dist/types/utils.d.ts +2 -0
- package/dist/utils.js +22 -3
- package/dist/validator.js +4 -2
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
[](https://github.com/kasimlyee/dotenv-gad/actions/workflows/ci.yml)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
|
+
[](https://bun.sh)
|
|
7
8
|
[](https://www.typescriptlang.org/)
|
|
8
9
|
[](https://kasimlyee.github.io/dotenv-gad/)
|
|
9
10
|
[](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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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}=${
|
|
52
|
+
envLines[lineIndex] = `${key}=${envValue}`;
|
|
47
53
|
}
|
|
48
54
|
else {
|
|
49
|
-
envLines.push(`${key}=${
|
|
55
|
+
envLines.push(`${key}=${envValue}`);
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
}
|
package/dist/crypto.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
|
58
|
-
cipher
|
|
59
|
-
|
|
60
|
-
const
|
|
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,
|
|
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
|
-
|
|
98
|
-
const
|
|
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
|
|
110
|
-
decipher
|
|
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 =
|
|
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
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
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,
|
|
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
|
|
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
|
|
238
|
-
|
|
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 =
|
|
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
|
|
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](
|
|
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
|
|
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
|
-
|
|
726
|
-
|
|
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
|
|
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 };
|
package/dist/runtime.js
ADDED
|
@@ -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
|
+
}
|
package/dist/schema-loader.js
CHANGED
|
@@ -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
|
|
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);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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](
|
|
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
|
|
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.
|
|
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",
|