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