dotenv-gad 1.4.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -11
- package/dist/cli/commands/check.js +10 -2
- package/dist/cli/commands/decrypt.js +126 -0
- package/dist/cli/commands/docs.js +2 -2
- package/dist/cli/commands/encrypt.js +100 -0
- package/dist/cli/commands/fix.js +16 -12
- package/dist/cli/commands/init.js +17 -12
- package/dist/cli/commands/keygen.js +97 -0
- package/dist/cli/commands/rotate.js +160 -0
- package/dist/cli/commands/status.js +116 -0
- package/dist/cli/commands/sync.js +15 -17
- package/dist/cli/commands/utils.js +40 -28
- package/dist/cli/commands/verify.js +101 -0
- package/dist/cli/index.js +12 -0
- package/dist/crypto.js +165 -0
- package/dist/errors.js +92 -4
- package/dist/index.cjs +408 -26
- package/dist/index.js +6 -4
- package/dist/runtime.js +43 -0
- package/dist/schema-loader.js +12 -3
- package/dist/types/cli/commands/check.d.ts +1 -1
- package/dist/types/cli/commands/decrypt.d.ts +2 -0
- package/dist/types/cli/commands/docs.d.ts +1 -1
- package/dist/types/cli/commands/encrypt.d.ts +2 -0
- package/dist/types/cli/commands/fix.d.ts +1 -1
- package/dist/types/cli/commands/init.d.ts +1 -1
- package/dist/types/cli/commands/keygen.d.ts +2 -0
- package/dist/types/cli/commands/rotate.d.ts +2 -0
- package/dist/types/cli/commands/status.d.ts +2 -0
- package/dist/types/cli/commands/sync.d.ts +1 -1
- package/dist/types/cli/commands/verify.d.ts +2 -0
- package/dist/types/crypto.d.ts +58 -0
- package/dist/types/errors.d.ts +35 -12
- package/dist/types/index.d.ts +8 -3
- package/dist/types/runtime.d.ts +29 -0
- package/dist/types/schema-loader.d.ts +5 -2
- package/dist/types/schema.d.ts +7 -0
- package/dist/types/utils.d.ts +21 -10
- package/dist/types/validator.d.ts +40 -0
- package/dist/utils.js +50 -13
- package/dist/validator.js +164 -19
- package/package.json +6 -4
package/dist/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
|
-
|
|
58
|
-
|
|
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
|
|
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(
|
|
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] =
|
|
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] :
|
|
120
|
-
if (groupedEnv[key] && Object.keys(groupedEnv[key]).length > 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 (
|
|
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 =
|
|
381
|
+
displayedValue = processedEnv[key];
|
|
147
382
|
}
|
|
148
383
|
} else {
|
|
149
|
-
displayedValue = this.redactValue(
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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)
|
|
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 (
|
|
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 (
|
|
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(
|
|
373
|
-
const
|
|
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 =
|
|
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 =
|
|
388
|
-
const
|
|
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 =
|
|
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
|
|
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 =
|
|
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);
|
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
|
+
}
|