node-safe-env 0.1.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/LICENSE +21 -0
- package/README.md +381 -0
- package/dist/cli.js +1019 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +938 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +196 -0
- package/dist/index.d.ts +196 -0
- package/dist/index.js +892 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
EnvValidationError: () => EnvValidationError,
|
|
34
|
+
createEnv: () => createEnv,
|
|
35
|
+
defineEnv: () => defineEnv,
|
|
36
|
+
maskEnv: () => maskEnv,
|
|
37
|
+
mergeSources: () => mergeSources,
|
|
38
|
+
readEnvFileSource: () => readEnvFileSource,
|
|
39
|
+
resolveExampleEnvPath: () => resolveExampleEnvPath,
|
|
40
|
+
traceEnv: () => traceEnv,
|
|
41
|
+
validateExampleEnv: () => validateExampleEnv,
|
|
42
|
+
validateExampleEnvFile: () => validateExampleEnvFile
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// src/errors/EnvValidationError.ts
|
|
47
|
+
var EnvValidationError = class extends Error {
|
|
48
|
+
constructor(issues) {
|
|
49
|
+
super(
|
|
50
|
+
[
|
|
51
|
+
"Environment validation failed:",
|
|
52
|
+
...issues.map((issue) => `- [${issue.code}] ${issue.message}`)
|
|
53
|
+
].join("\n")
|
|
54
|
+
);
|
|
55
|
+
this.name = "EnvValidationError";
|
|
56
|
+
this.issues = issues;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/createEnv.ts
|
|
61
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
62
|
+
|
|
63
|
+
// src/flattenSchema.ts
|
|
64
|
+
function isEnvRule(value) {
|
|
65
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
66
|
+
}
|
|
67
|
+
function toEnvKey(path3) {
|
|
68
|
+
return path3.map((segment) => segment.toUpperCase()).join("_");
|
|
69
|
+
}
|
|
70
|
+
function flattenSchema(schema, parentPath = []) {
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
73
|
+
const currentPath = [...parentPath, key];
|
|
74
|
+
if (isEnvRule(value)) {
|
|
75
|
+
entries.push({
|
|
76
|
+
path: currentPath,
|
|
77
|
+
envKey: toEnvKey(currentPath),
|
|
78
|
+
rule: value
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (typeof value === "object" && value !== null) {
|
|
83
|
+
entries.push(
|
|
84
|
+
...flattenSchema(value, currentPath)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/findUnknownEnvKeys.ts
|
|
92
|
+
function findUnknownEnvKeys(schema, source) {
|
|
93
|
+
const knownKeys = new Set(flattenSchema(schema).map((entry) => entry.envKey));
|
|
94
|
+
const issues = [];
|
|
95
|
+
for (const key of Object.keys(source)) {
|
|
96
|
+
if (source[key] === void 0) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (knownKeys.has(key)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
issues.push({
|
|
103
|
+
key,
|
|
104
|
+
code: "unknown_key",
|
|
105
|
+
message: `Environment variable "${key}" is not defined in the schema.`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return issues;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/loadEnvFiles.ts
|
|
112
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
113
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
114
|
+
var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
115
|
+
function stripInlineComment(input) {
|
|
116
|
+
let inSingleQuote = false;
|
|
117
|
+
let inDoubleQuote = false;
|
|
118
|
+
let isEscaped = false;
|
|
119
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
120
|
+
const char = input[index];
|
|
121
|
+
if (isEscaped) {
|
|
122
|
+
isEscaped = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (char === "\\") {
|
|
126
|
+
isEscaped = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (char === '"' && !inSingleQuote) {
|
|
130
|
+
inDoubleQuote = !inDoubleQuote;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (char === "'" && !inDoubleQuote) {
|
|
134
|
+
inSingleQuote = !inSingleQuote;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (char === "#" && !inSingleQuote && !inDoubleQuote && (index === 0 || /\s/.test(input[index - 1] ?? ""))) {
|
|
138
|
+
return input.slice(0, index).trimEnd();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return input.trimEnd();
|
|
142
|
+
}
|
|
143
|
+
function parseEnvFile(filePath) {
|
|
144
|
+
if (!import_node_fs.default.existsSync(filePath)) {
|
|
145
|
+
return /* @__PURE__ */ Object.create(null);
|
|
146
|
+
}
|
|
147
|
+
const content = import_node_fs.default.readFileSync(filePath, "utf8");
|
|
148
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
149
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
150
|
+
let line = rawLine.trim();
|
|
151
|
+
if (!line || line.startsWith("#")) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (line.startsWith("export ")) {
|
|
155
|
+
line = line.slice("export ".length).trim();
|
|
156
|
+
if (!line) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const equalIndex = line.indexOf("=");
|
|
161
|
+
if (equalIndex <= 0) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const key = line.slice(0, equalIndex).trim();
|
|
165
|
+
if (!ENV_KEY_PATTERN.test(key)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
let value = stripInlineComment(line.slice(equalIndex + 1)).trim();
|
|
169
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
170
|
+
value = value.slice(1, -1);
|
|
171
|
+
}
|
|
172
|
+
result[key] = value;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
function loadEnvFiles(options = {}) {
|
|
177
|
+
const cwd = options.cwd ?? process.cwd();
|
|
178
|
+
const nodeEnv = options.nodeEnv ?? process.env.NODE_ENV ?? "development";
|
|
179
|
+
const base = parseEnvFile(import_node_path.default.join(cwd, ".env"));
|
|
180
|
+
const local = parseEnvFile(import_node_path.default.join(cwd, ".env.local"));
|
|
181
|
+
const environment = parseEnvFile(import_node_path.default.join(cwd, `.env.${nodeEnv}`));
|
|
182
|
+
const custom = options.envFile ? parseEnvFile(import_node_path.default.resolve(cwd, options.envFile)) : /* @__PURE__ */ Object.create(null);
|
|
183
|
+
return {
|
|
184
|
+
base,
|
|
185
|
+
local,
|
|
186
|
+
environment,
|
|
187
|
+
custom
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/mergeSources.ts
|
|
192
|
+
function mergeSources(files, runtimeValues = process.env) {
|
|
193
|
+
return Object.assign(
|
|
194
|
+
/* @__PURE__ */ Object.create(null),
|
|
195
|
+
files.base,
|
|
196
|
+
files.local,
|
|
197
|
+
files.environment,
|
|
198
|
+
files.custom,
|
|
199
|
+
runtimeValues
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/applyTransform.ts
|
|
204
|
+
function applyTransform(key, rule, result) {
|
|
205
|
+
if (result.issue || result.value === void 0 || !rule.transform) {
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
return {
|
|
210
|
+
value: rule.transform(result.value)
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const message = error instanceof Error && error.message ? error.message : `Environment variable "${key}" failed transform.`;
|
|
214
|
+
return {
|
|
215
|
+
issue: {
|
|
216
|
+
key,
|
|
217
|
+
code: "invalid_custom",
|
|
218
|
+
message
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/validators/boolean.ts
|
|
225
|
+
var TRUE_VALUES = /* @__PURE__ */ new Set(["true", "1", "yes", "on"]);
|
|
226
|
+
var FALSE_VALUES = /* @__PURE__ */ new Set(["false", "0", "no", "off"]);
|
|
227
|
+
var validateBoolean = ({ key, rawValue }) => {
|
|
228
|
+
const normalized = rawValue.trim().toLowerCase();
|
|
229
|
+
if (TRUE_VALUES.has(normalized)) {
|
|
230
|
+
return { value: true };
|
|
231
|
+
}
|
|
232
|
+
if (FALSE_VALUES.has(normalized)) {
|
|
233
|
+
return { value: false };
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
issue: {
|
|
237
|
+
key,
|
|
238
|
+
code: "invalid_boolean",
|
|
239
|
+
message: `Environment variable "${key}" must be a valid boolean.`
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/validators/enum.ts
|
|
245
|
+
var validateEnum = ({ key, rawValue, rule }) => {
|
|
246
|
+
const enumRule = rule;
|
|
247
|
+
if (!enumRule.values.includes(rawValue)) {
|
|
248
|
+
return {
|
|
249
|
+
issue: {
|
|
250
|
+
key,
|
|
251
|
+
code: "invalid_enum",
|
|
252
|
+
message: `Environment variable "${key}" must be one of: ${enumRule.values.join(", ")}.`
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return { value: rawValue };
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/validators/json.ts
|
|
260
|
+
var validateJson = ({ key, rawValue }) => {
|
|
261
|
+
try {
|
|
262
|
+
const parsed = JSON.parse(rawValue);
|
|
263
|
+
return { value: parsed };
|
|
264
|
+
} catch {
|
|
265
|
+
return {
|
|
266
|
+
issue: {
|
|
267
|
+
key,
|
|
268
|
+
code: "invalid_json",
|
|
269
|
+
message: `Environment variable "${key}" must contain valid JSON.`
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/validators/number.ts
|
|
276
|
+
var validateNumber = ({ key, rawValue }) => {
|
|
277
|
+
const parsed = Number(rawValue);
|
|
278
|
+
if (!Number.isFinite(parsed)) {
|
|
279
|
+
return {
|
|
280
|
+
issue: {
|
|
281
|
+
key,
|
|
282
|
+
code: "invalid_number",
|
|
283
|
+
message: `Environment variable "${key}" must be a valid number.`
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return { value: parsed };
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/validators/port.ts
|
|
291
|
+
var validatePort = ({ key, rawValue }) => {
|
|
292
|
+
const num = Number(rawValue);
|
|
293
|
+
if (!Number.isInteger(num) || num < 1 || num > 65535) {
|
|
294
|
+
return {
|
|
295
|
+
issue: {
|
|
296
|
+
key,
|
|
297
|
+
code: "invalid_port",
|
|
298
|
+
message: `Environment variable "${key}" must be a valid port (1\u201365535).`
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return { value: num };
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// src/validators/string.ts
|
|
306
|
+
var validateString = ({ rawValue }) => {
|
|
307
|
+
return { value: rawValue };
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/validators/url.ts
|
|
311
|
+
var validateUrl = ({ key, rawValue }) => {
|
|
312
|
+
try {
|
|
313
|
+
new URL(rawValue);
|
|
314
|
+
return { value: rawValue };
|
|
315
|
+
} catch {
|
|
316
|
+
return {
|
|
317
|
+
issue: {
|
|
318
|
+
key,
|
|
319
|
+
code: "invalid_url",
|
|
320
|
+
message: `Environment variable "${key}" must be a valid URL.`
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/validators/int.ts
|
|
327
|
+
var validateInt = ({ key, rawValue }) => {
|
|
328
|
+
const parsed = Number(rawValue);
|
|
329
|
+
if (!Number.isInteger(parsed)) {
|
|
330
|
+
return {
|
|
331
|
+
issue: {
|
|
332
|
+
key,
|
|
333
|
+
code: "invalid_number",
|
|
334
|
+
message: `Environment variable "${key}" must be a valid integer.`
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return { value: parsed };
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// src/validators/float.ts
|
|
342
|
+
var validateFloat = ({ key, rawValue }) => {
|
|
343
|
+
const parsed = Number(rawValue);
|
|
344
|
+
if (!Number.isFinite(parsed)) {
|
|
345
|
+
return {
|
|
346
|
+
issue: {
|
|
347
|
+
key,
|
|
348
|
+
code: "invalid_number",
|
|
349
|
+
message: `Environment variable "${key}" must be a valid float.`
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return { value: parsed };
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/validators/array.ts
|
|
357
|
+
var validateArray = ({ key, rawValue, rule }) => {
|
|
358
|
+
const arrayRule = rule;
|
|
359
|
+
const separator = arrayRule.separator ?? ",";
|
|
360
|
+
const trimItems = arrayRule.trimItems ?? true;
|
|
361
|
+
const allowEmptyItems = arrayRule.allowEmptyItems ?? false;
|
|
362
|
+
const splitValues = rawValue.split(separator);
|
|
363
|
+
const parsed = trimItems ? splitValues.map((item) => item.trim()) : splitValues;
|
|
364
|
+
if (!allowEmptyItems && parsed.some((item) => item === "")) {
|
|
365
|
+
const issue = {
|
|
366
|
+
key,
|
|
367
|
+
code: "invalid_array",
|
|
368
|
+
message: `Environment variable "${key}" cannot contain empty array items`
|
|
369
|
+
};
|
|
370
|
+
return { issue };
|
|
371
|
+
}
|
|
372
|
+
return { value: parsed };
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/validators/custom.ts
|
|
376
|
+
var validateCustom = ({ key, rawValue, rule }) => {
|
|
377
|
+
const customRule = rule;
|
|
378
|
+
try {
|
|
379
|
+
const parsed = customRule.parse(rawValue);
|
|
380
|
+
return { value: parsed };
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const message = error instanceof Error && error.message ? error.message : `Environment variable "${key}" failed custom validation.`;
|
|
383
|
+
return {
|
|
384
|
+
issue: {
|
|
385
|
+
key,
|
|
386
|
+
code: "invalid_custom",
|
|
387
|
+
message
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// src/validators/email.ts
|
|
394
|
+
var validateEmail = ({ key, rawValue, rule }) => {
|
|
395
|
+
const emailRule = rule;
|
|
396
|
+
void emailRule;
|
|
397
|
+
const value = rawValue.trim();
|
|
398
|
+
if (!value) {
|
|
399
|
+
const issue = {
|
|
400
|
+
key,
|
|
401
|
+
code: "invalid_email",
|
|
402
|
+
message: `Environment variable "${key}" must be a valid email address`
|
|
403
|
+
};
|
|
404
|
+
return { issue };
|
|
405
|
+
}
|
|
406
|
+
if (/\s/.test(value)) {
|
|
407
|
+
const issue = {
|
|
408
|
+
key,
|
|
409
|
+
code: "invalid_email",
|
|
410
|
+
message: `Environment variable "${key}" must be a valid email address`
|
|
411
|
+
};
|
|
412
|
+
return { issue };
|
|
413
|
+
}
|
|
414
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
415
|
+
if (!emailRegex.test(value)) {
|
|
416
|
+
const issue = {
|
|
417
|
+
key,
|
|
418
|
+
code: "invalid_email",
|
|
419
|
+
message: `Environment variable "${key}" must be a valid email address`
|
|
420
|
+
};
|
|
421
|
+
return { issue };
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
value
|
|
425
|
+
};
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// src/validators/date.ts
|
|
429
|
+
function isValidDate(date) {
|
|
430
|
+
return !Number.isNaN(date.getTime());
|
|
431
|
+
}
|
|
432
|
+
var validateDate = ({ key, rawValue, rule }) => {
|
|
433
|
+
const dateRule = rule;
|
|
434
|
+
void dateRule;
|
|
435
|
+
const value = rawValue.trim();
|
|
436
|
+
if (!value) {
|
|
437
|
+
const issue = {
|
|
438
|
+
key,
|
|
439
|
+
code: "invalid_date",
|
|
440
|
+
message: `Environment variable "${key}" must be a valid ISO date`
|
|
441
|
+
};
|
|
442
|
+
return { issue };
|
|
443
|
+
}
|
|
444
|
+
const isoDateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
445
|
+
const isoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
446
|
+
if (!isoDateRegex.test(value) && !isoDateTimeRegex.test(value)) {
|
|
447
|
+
const issue = {
|
|
448
|
+
key,
|
|
449
|
+
code: "invalid_date",
|
|
450
|
+
message: `Environment variable "${key}" must be a valid ISO date string`
|
|
451
|
+
};
|
|
452
|
+
return { issue };
|
|
453
|
+
}
|
|
454
|
+
const parsed = new Date(value);
|
|
455
|
+
if (!isValidDate(parsed)) {
|
|
456
|
+
const issue = {
|
|
457
|
+
key,
|
|
458
|
+
code: "invalid_date",
|
|
459
|
+
message: `Environment variable "${key}" must be a valid date`
|
|
460
|
+
};
|
|
461
|
+
return { issue };
|
|
462
|
+
}
|
|
463
|
+
if (isoDateRegex.test(value)) {
|
|
464
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
465
|
+
if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() + 1 !== month || parsed.getUTCDate() !== day) {
|
|
466
|
+
const issue = {
|
|
467
|
+
key,
|
|
468
|
+
code: "invalid_date",
|
|
469
|
+
message: `Environment variable "${key}" must be a real calendar date`
|
|
470
|
+
};
|
|
471
|
+
return { issue };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
value: parsed
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// src/validators/index.ts
|
|
480
|
+
var validators = {
|
|
481
|
+
string: validateString,
|
|
482
|
+
number: validateNumber,
|
|
483
|
+
boolean: validateBoolean,
|
|
484
|
+
enum: validateEnum,
|
|
485
|
+
url: validateUrl,
|
|
486
|
+
port: validatePort,
|
|
487
|
+
json: validateJson,
|
|
488
|
+
int: validateInt,
|
|
489
|
+
float: validateFloat,
|
|
490
|
+
array: validateArray,
|
|
491
|
+
custom: validateCustom,
|
|
492
|
+
email: validateEmail,
|
|
493
|
+
date: validateDate
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/parseValue.ts
|
|
497
|
+
function parseValue(key, rawValue, rule) {
|
|
498
|
+
const validator = validators[rule.type];
|
|
499
|
+
const result = validator({
|
|
500
|
+
key,
|
|
501
|
+
rawValue,
|
|
502
|
+
rule
|
|
503
|
+
});
|
|
504
|
+
return applyTransform(key, rule, result);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/setNestedValue.ts
|
|
508
|
+
function setNestedValue(target, path3, value) {
|
|
509
|
+
let current = target;
|
|
510
|
+
for (let index = 0; index < path3.length - 1; index += 1) {
|
|
511
|
+
const segment = path3[index];
|
|
512
|
+
const existing = current[segment];
|
|
513
|
+
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
|
|
514
|
+
current[segment] = {};
|
|
515
|
+
}
|
|
516
|
+
current = current[segment];
|
|
517
|
+
}
|
|
518
|
+
current[path3[path3.length - 1]] = value;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/createEnv.ts
|
|
522
|
+
function isEmptyString(value) {
|
|
523
|
+
return value.trim() === "";
|
|
524
|
+
}
|
|
525
|
+
function defaultToRawValue(value) {
|
|
526
|
+
if (typeof value === "string") {
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
530
|
+
return String(value);
|
|
531
|
+
}
|
|
532
|
+
if (value instanceof Date) {
|
|
533
|
+
return value.toISOString();
|
|
534
|
+
}
|
|
535
|
+
return JSON.stringify(value);
|
|
536
|
+
}
|
|
537
|
+
function resolveDefaultValue(value) {
|
|
538
|
+
return typeof value === "function" ? value() : value;
|
|
539
|
+
}
|
|
540
|
+
function parseDefaultValue(envKey, rule) {
|
|
541
|
+
const defaultKind = typeof rule.default === "function" ? "function" : "static";
|
|
542
|
+
let resolvedDefault;
|
|
543
|
+
try {
|
|
544
|
+
resolvedDefault = resolveDefaultValue(rule.default);
|
|
545
|
+
} catch (err) {
|
|
546
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
547
|
+
return {
|
|
548
|
+
issue: {
|
|
549
|
+
key: envKey,
|
|
550
|
+
code: "invalid_default",
|
|
551
|
+
message: `Default function for "${envKey}" threw an error: ${message}`
|
|
552
|
+
},
|
|
553
|
+
defaultKind
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const rawDefault = defaultToRawValue(resolvedDefault);
|
|
557
|
+
return {
|
|
558
|
+
...parseValue(envKey, rawDefault, rule),
|
|
559
|
+
defaultKind,
|
|
560
|
+
rawDefault
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function countKeys(source) {
|
|
564
|
+
return Object.values(source).filter((value) => typeof value === "string").length;
|
|
565
|
+
}
|
|
566
|
+
function buildLoadedFileReport(loadedFiles, cwd, nodeEnv, envFile) {
|
|
567
|
+
return [
|
|
568
|
+
{
|
|
569
|
+
source: ".env",
|
|
570
|
+
path: import_node_path2.default.join(cwd, ".env"),
|
|
571
|
+
keyCount: countKeys(loadedFiles.base)
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
source: ".env.local",
|
|
575
|
+
path: import_node_path2.default.join(cwd, ".env.local"),
|
|
576
|
+
keyCount: countKeys(loadedFiles.local)
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
source: ".env.environment",
|
|
580
|
+
path: import_node_path2.default.join(cwd, `.env.${nodeEnv}`),
|
|
581
|
+
keyCount: countKeys(loadedFiles.environment)
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
source: "custom",
|
|
585
|
+
path: envFile ? import_node_path2.default.resolve(cwd, envFile) : void 0,
|
|
586
|
+
keyCount: countKeys(loadedFiles.custom)
|
|
587
|
+
}
|
|
588
|
+
];
|
|
589
|
+
}
|
|
590
|
+
function buildSourceTrace(loadedFiles, runtimeValues) {
|
|
591
|
+
const trace = /* @__PURE__ */ Object.create(null);
|
|
592
|
+
const applySource = (source, label) => {
|
|
593
|
+
for (const [key, raw] of Object.entries(source)) {
|
|
594
|
+
if (typeof raw === "string") {
|
|
595
|
+
trace[key] = { source: label, raw };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
applySource(loadedFiles.base, ".env");
|
|
600
|
+
applySource(loadedFiles.local, ".env.local");
|
|
601
|
+
applySource(loadedFiles.environment, ".env.environment");
|
|
602
|
+
applySource(loadedFiles.custom, "custom");
|
|
603
|
+
applySource(runtimeValues, "process.env");
|
|
604
|
+
return trace;
|
|
605
|
+
}
|
|
606
|
+
function maskDebugValue(value, sensitive) {
|
|
607
|
+
if (value === void 0) {
|
|
608
|
+
return void 0;
|
|
609
|
+
}
|
|
610
|
+
return sensitive ? "***" : value;
|
|
611
|
+
}
|
|
612
|
+
function resolveDebugLogger(debug) {
|
|
613
|
+
if (debug === true) {
|
|
614
|
+
return (report) => {
|
|
615
|
+
console.info(report);
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
if (debug && typeof debug === "object" && debug.logger) {
|
|
619
|
+
return debug.logger;
|
|
620
|
+
}
|
|
621
|
+
return void 0;
|
|
622
|
+
}
|
|
623
|
+
function createEnv(schema, options = {}) {
|
|
624
|
+
const debugEnabled = options.debug !== void 0 && options.debug !== false;
|
|
625
|
+
const debugLogger = debugEnabled ? resolveDebugLogger(options.debug) : void 0;
|
|
626
|
+
const debugKeys = [];
|
|
627
|
+
let source;
|
|
628
|
+
let loadedFileReport = [];
|
|
629
|
+
let sourceTrace = /* @__PURE__ */ Object.create(null);
|
|
630
|
+
if (options.source) {
|
|
631
|
+
source = options.source;
|
|
632
|
+
if (debugEnabled) {
|
|
633
|
+
for (const [key, raw] of Object.entries(source)) {
|
|
634
|
+
if (typeof raw === "string") {
|
|
635
|
+
sourceTrace[key] = { source: "process.env", raw };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
const loadedFiles = loadEnvFiles({
|
|
641
|
+
cwd: options.cwd,
|
|
642
|
+
nodeEnv: options.nodeEnv,
|
|
643
|
+
envFile: options.envFile
|
|
644
|
+
});
|
|
645
|
+
source = mergeSources(loadedFiles, process.env);
|
|
646
|
+
if (debugEnabled) {
|
|
647
|
+
const cwd = options.cwd ?? process.cwd();
|
|
648
|
+
const nodeEnv = options.nodeEnv ?? process.env.NODE_ENV ?? "development";
|
|
649
|
+
loadedFileReport = buildLoadedFileReport(
|
|
650
|
+
loadedFiles,
|
|
651
|
+
cwd,
|
|
652
|
+
nodeEnv,
|
|
653
|
+
options.envFile
|
|
654
|
+
);
|
|
655
|
+
sourceTrace = buildSourceTrace(loadedFiles, process.env);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const issues = [];
|
|
659
|
+
const result = {};
|
|
660
|
+
const flattenedSchema = flattenSchema(schema);
|
|
661
|
+
if (options.strict) {
|
|
662
|
+
issues.push(
|
|
663
|
+
...findUnknownEnvKeys(schema, source)
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
for (const entry of flattenedSchema) {
|
|
667
|
+
const { path: path3, envKey, rule } = entry;
|
|
668
|
+
const currentValue = source[envKey];
|
|
669
|
+
const sourceInfo = sourceTrace[envKey];
|
|
670
|
+
const sensitive = rule.sensitive === true;
|
|
671
|
+
const defaultKind = rule.default === void 0 ? void 0 : typeof rule.default === "function" ? "function" : "static";
|
|
672
|
+
const pushDebugEntry = (status, values) => {
|
|
673
|
+
if (!debugEnabled) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
debugKeys.push({
|
|
677
|
+
key: envKey,
|
|
678
|
+
ruleType: rule.type,
|
|
679
|
+
source: values.source,
|
|
680
|
+
usedDefault: values.usedDefault,
|
|
681
|
+
defaultKind: values.defaultKind,
|
|
682
|
+
raw: maskDebugValue(values.raw, sensitive),
|
|
683
|
+
parsed: maskDebugValue(values.parsed, sensitive),
|
|
684
|
+
status,
|
|
685
|
+
issue: values.issue
|
|
686
|
+
});
|
|
687
|
+
};
|
|
688
|
+
if (typeof currentValue !== "string") {
|
|
689
|
+
if (rule.default !== void 0) {
|
|
690
|
+
const parsedDefault = parseDefaultValue(envKey, rule);
|
|
691
|
+
if (parsedDefault.issue) {
|
|
692
|
+
issues.push(parsedDefault.issue);
|
|
693
|
+
pushDebugEntry("issue", {
|
|
694
|
+
source: "default",
|
|
695
|
+
usedDefault: true,
|
|
696
|
+
defaultKind: parsedDefault.defaultKind,
|
|
697
|
+
raw: parsedDefault.rawDefault,
|
|
698
|
+
issue: parsedDefault.issue
|
|
699
|
+
});
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
pushDebugEntry("defaulted", {
|
|
703
|
+
source: "default",
|
|
704
|
+
usedDefault: true,
|
|
705
|
+
defaultKind: parsedDefault.defaultKind,
|
|
706
|
+
raw: parsedDefault.rawDefault,
|
|
707
|
+
parsed: parsedDefault.value
|
|
708
|
+
});
|
|
709
|
+
setNestedValue(result, path3, parsedDefault.value);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (rule.required) {
|
|
713
|
+
const issue = {
|
|
714
|
+
key: envKey,
|
|
715
|
+
code: "missing",
|
|
716
|
+
message: `Missing required environment variable "${envKey}".`
|
|
717
|
+
};
|
|
718
|
+
issues.push(issue);
|
|
719
|
+
pushDebugEntry("missing", {
|
|
720
|
+
source: "missing",
|
|
721
|
+
usedDefault: false,
|
|
722
|
+
defaultKind,
|
|
723
|
+
issue
|
|
724
|
+
});
|
|
725
|
+
} else {
|
|
726
|
+
pushDebugEntry("missing", {
|
|
727
|
+
source: "missing",
|
|
728
|
+
usedDefault: false,
|
|
729
|
+
defaultKind
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const rawValue = currentValue;
|
|
735
|
+
if (!rule.allowEmpty && isEmptyString(rawValue)) {
|
|
736
|
+
if (rule.default !== void 0) {
|
|
737
|
+
const parsedDefault = parseDefaultValue(envKey, rule);
|
|
738
|
+
if (parsedDefault.issue) {
|
|
739
|
+
issues.push(parsedDefault.issue);
|
|
740
|
+
pushDebugEntry("issue", {
|
|
741
|
+
source: "default",
|
|
742
|
+
usedDefault: true,
|
|
743
|
+
defaultKind: parsedDefault.defaultKind,
|
|
744
|
+
raw: parsedDefault.rawDefault,
|
|
745
|
+
issue: parsedDefault.issue
|
|
746
|
+
});
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
pushDebugEntry("defaulted", {
|
|
750
|
+
source: "default",
|
|
751
|
+
usedDefault: true,
|
|
752
|
+
defaultKind: parsedDefault.defaultKind,
|
|
753
|
+
raw: parsedDefault.rawDefault,
|
|
754
|
+
parsed: parsedDefault.value
|
|
755
|
+
});
|
|
756
|
+
setNestedValue(result, path3, parsedDefault.value);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const issue = {
|
|
760
|
+
key: envKey,
|
|
761
|
+
code: "empty",
|
|
762
|
+
message: `Environment variable "${envKey}" cannot be empty.`
|
|
763
|
+
};
|
|
764
|
+
issues.push(issue);
|
|
765
|
+
pushDebugEntry("empty", {
|
|
766
|
+
source: sourceInfo?.source ?? "process.env",
|
|
767
|
+
usedDefault: false,
|
|
768
|
+
defaultKind,
|
|
769
|
+
raw: rawValue,
|
|
770
|
+
issue
|
|
771
|
+
});
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
const parsed = parseValue(envKey, rawValue, rule);
|
|
775
|
+
if (parsed.issue) {
|
|
776
|
+
issues.push(parsed.issue);
|
|
777
|
+
pushDebugEntry("issue", {
|
|
778
|
+
source: sourceInfo?.source ?? "process.env",
|
|
779
|
+
usedDefault: false,
|
|
780
|
+
defaultKind,
|
|
781
|
+
raw: rawValue,
|
|
782
|
+
issue: parsed.issue
|
|
783
|
+
});
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
pushDebugEntry("parsed", {
|
|
787
|
+
source: sourceInfo?.source ?? "process.env",
|
|
788
|
+
usedDefault: false,
|
|
789
|
+
defaultKind,
|
|
790
|
+
raw: rawValue,
|
|
791
|
+
parsed: parsed.value
|
|
792
|
+
});
|
|
793
|
+
setNestedValue(result, path3, parsed.value);
|
|
794
|
+
}
|
|
795
|
+
if (debugLogger) {
|
|
796
|
+
debugLogger({
|
|
797
|
+
loadedFiles: loadedFileReport,
|
|
798
|
+
keys: debugKeys
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
if (issues.length > 0) {
|
|
802
|
+
throw new EnvValidationError(issues);
|
|
803
|
+
}
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/defineEnv.ts
|
|
808
|
+
function defineEnv(schema) {
|
|
809
|
+
return schema;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/maskEnv.ts
|
|
813
|
+
function maskValue() {
|
|
814
|
+
return "***";
|
|
815
|
+
}
|
|
816
|
+
function maskEnv(schema, env) {
|
|
817
|
+
const masked = {};
|
|
818
|
+
const envRecord = env;
|
|
819
|
+
for (const key of Object.keys(schema)) {
|
|
820
|
+
const value = envRecord[key];
|
|
821
|
+
if (value === void 0) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
masked[key] = schema[key].sensitive ? maskValue() : value;
|
|
825
|
+
}
|
|
826
|
+
return masked;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/traceEnv.ts
|
|
830
|
+
function traceEnv(schema, sources, env) {
|
|
831
|
+
const traced = {};
|
|
832
|
+
const envRecord = env;
|
|
833
|
+
for (const key of Object.keys(schema)) {
|
|
834
|
+
const value = envRecord[key];
|
|
835
|
+
if (value === void 0) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const sourceInfo = sources[key];
|
|
839
|
+
if (!sourceInfo) {
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
traced[key] = {
|
|
843
|
+
source: sourceInfo.source,
|
|
844
|
+
raw: sourceInfo.raw,
|
|
845
|
+
parsed: value
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return traced;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/validateExampleEnv.ts
|
|
852
|
+
function validateExampleEnv(schema, exampleSource) {
|
|
853
|
+
const issues = [];
|
|
854
|
+
const flattenedSchema = flattenSchema(schema);
|
|
855
|
+
const expectedKeys = new Set(flattenedSchema.map((entry) => entry.envKey));
|
|
856
|
+
for (const entry of flattenedSchema) {
|
|
857
|
+
const { envKey } = entry;
|
|
858
|
+
if (!(envKey in exampleSource)) {
|
|
859
|
+
issues.push({
|
|
860
|
+
key: envKey,
|
|
861
|
+
code: "missing_example_key",
|
|
862
|
+
message: `Environment variable "${envKey}" is missing from .env.example.`
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
for (const key of Object.keys(exampleSource)) {
|
|
867
|
+
if (exampleSource[key] === void 0) {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (expectedKeys.has(key)) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
issues.push({
|
|
874
|
+
key,
|
|
875
|
+
code: "unknown_example_key",
|
|
876
|
+
message: `Environment variable "${key}" in .env.example is not defined in the schema.`
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
return issues;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/readEnvFileSource.ts
|
|
883
|
+
var import_node_fs2 = require("fs");
|
|
884
|
+
var import_node_path3 = require("path");
|
|
885
|
+
function stripQuotes(value) {
|
|
886
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
887
|
+
return value.slice(1, -1);
|
|
888
|
+
}
|
|
889
|
+
return value;
|
|
890
|
+
}
|
|
891
|
+
function readEnvFileSource(filePath) {
|
|
892
|
+
if (!(0, import_node_fs2.existsSync)(filePath)) {
|
|
893
|
+
return {};
|
|
894
|
+
}
|
|
895
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf8");
|
|
896
|
+
const source = {};
|
|
897
|
+
for (const line of content.split(/\r?\n/)) {
|
|
898
|
+
const trimmed = line.trim();
|
|
899
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const equalsIndex = trimmed.indexOf("=");
|
|
903
|
+
if (equalsIndex === -1) {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const key = trimmed.slice(0, equalsIndex).trim();
|
|
907
|
+
const rawValue = trimmed.slice(equalsIndex + 1).trim();
|
|
908
|
+
if (!key) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
source[key] = stripQuotes(rawValue);
|
|
912
|
+
}
|
|
913
|
+
return source;
|
|
914
|
+
}
|
|
915
|
+
function resolveExampleEnvPath(cwd = process.cwd(), exampleFile = ".env.example") {
|
|
916
|
+
return (0, import_node_path3.resolve)(cwd, exampleFile);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/validateExampleEnvFile.ts
|
|
920
|
+
function validateExampleEnvFile(schema, options = {}) {
|
|
921
|
+
const filePath = resolveExampleEnvPath(options.cwd, options.exampleFile);
|
|
922
|
+
const exampleSource = readEnvFileSource(filePath);
|
|
923
|
+
return validateExampleEnv(schema, exampleSource);
|
|
924
|
+
}
|
|
925
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
926
|
+
0 && (module.exports = {
|
|
927
|
+
EnvValidationError,
|
|
928
|
+
createEnv,
|
|
929
|
+
defineEnv,
|
|
930
|
+
maskEnv,
|
|
931
|
+
mergeSources,
|
|
932
|
+
readEnvFileSource,
|
|
933
|
+
resolveExampleEnvPath,
|
|
934
|
+
traceEnv,
|
|
935
|
+
validateExampleEnv,
|
|
936
|
+
validateExampleEnvFile
|
|
937
|
+
});
|
|
938
|
+
//# sourceMappingURL=index.cjs.map
|