@arkenv/core 1.0.0-alpha.3 → 1.0.0-alpha.5
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 +3 -0
- package/README.md +19 -10
- package/dist/index.cjs +438 -196
- package/dist/index.d.cts +191 -83
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +191 -83
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +436 -194
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -6
package/dist/index.cjs
CHANGED
|
@@ -2,243 +2,490 @@ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toString
|
|
|
2
2
|
let arktype = require("arktype");
|
|
3
3
|
|
|
4
4
|
//#region ../internal/scope/dist/index.js
|
|
5
|
-
const n
|
|
5
|
+
const n = (0, arktype.type)(`0 <= number.integer <= 65535`), r = (0, arktype.type)(`string.ip | 'localhost'`), i = (0, arktype.scope)({
|
|
6
6
|
string: arktype.type.module({
|
|
7
7
|
...arktype.type.keywords.string,
|
|
8
|
-
host: r
|
|
8
|
+
host: r
|
|
9
9
|
}),
|
|
10
10
|
number: arktype.type.module({
|
|
11
11
|
...arktype.type.keywords.number,
|
|
12
|
-
port: n
|
|
12
|
+
port: n
|
|
13
13
|
})
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region ../internal/utils/dist/boundary-access-error.js
|
|
18
|
+
/**
|
|
19
|
+
* `error.name` for the validation class {@link ArkEnvError}.
|
|
20
|
+
*/
|
|
21
|
+
const ARKENV_ERROR_NAME = "ArkEnvError";
|
|
22
|
+
|
|
16
23
|
//#endregion
|
|
17
24
|
//#region ../internal/utils/dist/index.js
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Attempt to coerce a value to a number.
|
|
27
|
+
*
|
|
28
|
+
* If the input is already a number, returns it unchanged.
|
|
29
|
+
* If the input is a string that can be parsed as a number, returns the parsed number.
|
|
30
|
+
* Otherwise, returns the original value unchanged.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
* @param s - The value to coerce
|
|
34
|
+
* @returns The coerced number or the original value
|
|
35
|
+
*/
|
|
36
|
+
const coerceNumber = (s) => {
|
|
37
|
+
if (typeof s === "number") return s;
|
|
38
|
+
if (typeof s !== "string" || !s.trim()) return s;
|
|
39
|
+
if (s.trim() === "NaN") return NaN;
|
|
40
|
+
const n = Number(s);
|
|
41
|
+
return Number.isNaN(n) ? s : n;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Attempt to coerce a value to a boolean.
|
|
45
|
+
*
|
|
46
|
+
* Convert the strings "true" and "false" to their boolean equivalents.
|
|
47
|
+
* All other values are returned unchanged.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
* @param s - The value to coerce
|
|
51
|
+
* @returns The coerced boolean or the original value
|
|
52
|
+
*/
|
|
53
|
+
const coerceBoolean = (s) => {
|
|
54
|
+
if (s === "true") return true;
|
|
55
|
+
if (s === "false") return false;
|
|
56
|
+
return s;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Attempt to parse a value as JSON.
|
|
60
|
+
*
|
|
61
|
+
* If the input is a string that starts with `{` or `[` and can be parsed as JSON,
|
|
62
|
+
* returns the parsed object or array. Otherwise, returns the original value unchanged.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
* @param s - The value to parse
|
|
66
|
+
* @returns The parsed JSON or the original value
|
|
67
|
+
*/
|
|
68
|
+
const coerceJson = (s) => {
|
|
69
|
+
if (typeof s !== "string") return s;
|
|
70
|
+
const trimmed = s.trim();
|
|
71
|
+
if (trimmed[0] !== "{" && trimmed[0] !== "[") return s;
|
|
27
72
|
try {
|
|
28
|
-
return JSON.parse(
|
|
73
|
+
return JSON.parse(trimmed);
|
|
29
74
|
} catch {
|
|
30
|
-
return
|
|
75
|
+
return s;
|
|
31
76
|
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Attempt to coerce a value to a Date.
|
|
80
|
+
*
|
|
81
|
+
* If the input is already a Date, returns it unchanged.
|
|
82
|
+
* If the input is a valid date string, returns a Date object.
|
|
83
|
+
* Otherwise, returns the original value unchanged.
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
86
|
+
* @param s - The value to coerce
|
|
87
|
+
* @returns The coerced Date or the original value
|
|
88
|
+
*/
|
|
89
|
+
const coerceDate = (s) => {
|
|
90
|
+
if (s instanceof Date) return s;
|
|
91
|
+
if (typeof s !== "string" || !s.trim()) return s;
|
|
92
|
+
const d = new Date(s);
|
|
93
|
+
return Number.isNaN(d.getTime()) ? s : d;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Remove keys with empty string values from an environment record.
|
|
97
|
+
*
|
|
98
|
+
* When a key is set to `""` (e.g. `PORT=` in a `.env` file), deleting it
|
|
99
|
+
* allows the validator to treat it as missing so that defaults apply.
|
|
100
|
+
*
|
|
101
|
+
* @param env The environment variables record
|
|
102
|
+
* @returns A new record with empty string keys removed
|
|
103
|
+
*/
|
|
104
|
+
const stripEmptyStrings = (env) => {
|
|
105
|
+
const result = {};
|
|
106
|
+
for (const key in env) {
|
|
107
|
+
const value = env[key];
|
|
108
|
+
if (value !== "") result[key] = value;
|
|
41
109
|
}
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Find all paths in a JSON Schema that require coercion.
|
|
114
|
+
*
|
|
115
|
+
* Prioritize "number", "integer", "boolean", "array", "object", and "date" types.
|
|
116
|
+
*
|
|
117
|
+
* @param node The JSON Schema node to traverse
|
|
118
|
+
* @param path The current path segments in the schema tree
|
|
119
|
+
* @returns An array of coercion targets containing their path and type
|
|
120
|
+
*/
|
|
121
|
+
const findCoercionPaths = (node, path = []) => {
|
|
122
|
+
const results = [];
|
|
123
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return results;
|
|
124
|
+
const n = node;
|
|
125
|
+
if ("const" in n) {
|
|
126
|
+
const t = typeof n.const;
|
|
127
|
+
if (t === "number" || t === "boolean") results.push({
|
|
128
|
+
path: [...path],
|
|
129
|
+
type: "primitive"
|
|
52
130
|
});
|
|
53
131
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
132
|
+
if ("enum" in n && Array.isArray(n.enum)) {
|
|
133
|
+
if (n.enum.some((v) => typeof v === "number" || typeof v === "boolean")) results.push({
|
|
134
|
+
path: [...path],
|
|
135
|
+
type: "primitive"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const type = n.type;
|
|
139
|
+
if (type === "number" || type === "integer" || type === "boolean") results.push({
|
|
140
|
+
path: [...path],
|
|
141
|
+
type: "primitive"
|
|
62
142
|
});
|
|
63
|
-
else if (
|
|
64
|
-
path: [...
|
|
65
|
-
type:
|
|
143
|
+
else if (type === "string" && "format" in n && (n.format === "date-time" || n.format === "date")) results.push({
|
|
144
|
+
path: [...path],
|
|
145
|
+
type: "date"
|
|
66
146
|
});
|
|
67
|
-
else if (
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
path: [...
|
|
71
|
-
type:
|
|
147
|
+
else if (type === "object") {
|
|
148
|
+
if (n.properties && Object.keys(n.properties).length > 0) {
|
|
149
|
+
results.push({
|
|
150
|
+
path: [...path],
|
|
151
|
+
type: "object"
|
|
72
152
|
});
|
|
73
|
-
for (
|
|
153
|
+
for (const key in n.properties) results.push(...findCoercionPaths(n.properties[key], [...path, key]));
|
|
74
154
|
}
|
|
75
|
-
} else
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
155
|
+
} else if (type === "array") {
|
|
156
|
+
results.push({
|
|
157
|
+
path: [...path],
|
|
158
|
+
type: "array"
|
|
159
|
+
});
|
|
160
|
+
if (n.items) if (Array.isArray(n.items)) n.items.forEach((item, index) => {
|
|
161
|
+
results.push(...findCoercionPaths(item, [...path, String(index)]));
|
|
162
|
+
});
|
|
163
|
+
else results.push(...findCoercionPaths(n.items, [...path, "*"]));
|
|
164
|
+
}
|
|
165
|
+
for (const comb of [
|
|
166
|
+
"anyOf",
|
|
167
|
+
"allOf",
|
|
168
|
+
"oneOf"
|
|
169
|
+
]) if (n[comb] && Array.isArray(n[comb])) for (const branch of n[comb]) results.push(...findCoercionPaths(branch, path));
|
|
170
|
+
const seen = /* @__PURE__ */ new Set();
|
|
171
|
+
return results.filter((t) => {
|
|
172
|
+
const key = t.path.join("/") + ":" + t.type;
|
|
173
|
+
return seen.has(key) ? false : seen.add(key);
|
|
90
174
|
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Apply coercion to a data object based on identified paths.
|
|
178
|
+
*
|
|
179
|
+
* @param data The input environment data object to coerce
|
|
180
|
+
* @param targets The coercion targets mapping paths to types
|
|
181
|
+
* @param options The coercion options, including array parsing format
|
|
182
|
+
* @returns The coerced data object
|
|
183
|
+
*/
|
|
184
|
+
const applyCoercion = (data, targets, options = {}) => {
|
|
185
|
+
const { arrayFormat = "comma" } = options;
|
|
186
|
+
const splitString = (val) => {
|
|
187
|
+
if (arrayFormat === "json") try {
|
|
188
|
+
return JSON.parse(val);
|
|
95
189
|
} catch {
|
|
96
|
-
return
|
|
190
|
+
return val;
|
|
97
191
|
}
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
if (
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
192
|
+
return val.trim() ? val.split(",").map((s) => s.trim()) : [];
|
|
193
|
+
};
|
|
194
|
+
const coerceValue = (val, type) => {
|
|
195
|
+
if (type === "array" && typeof val === "string") return splitString(val);
|
|
196
|
+
if (type === "object" && typeof val === "string") return coerceJson(val);
|
|
197
|
+
if (type === "date" && typeof val === "string") return coerceDate(val);
|
|
198
|
+
if (type === "primitive") {
|
|
199
|
+
if (Array.isArray(val)) return val.map((item) => {
|
|
200
|
+
if (typeof item !== "string") return item;
|
|
201
|
+
const n = coerceNumber(item);
|
|
202
|
+
return typeof n === "number" ? n : coerceBoolean(item);
|
|
108
203
|
});
|
|
109
|
-
if (typeof
|
|
110
|
-
|
|
111
|
-
return typeof n
|
|
204
|
+
if (typeof val !== "string") return val;
|
|
205
|
+
const n = coerceNumber(val);
|
|
206
|
+
return typeof n === "number" ? n : coerceBoolean(val);
|
|
112
207
|
}
|
|
113
|
-
return
|
|
208
|
+
return val;
|
|
114
209
|
};
|
|
115
|
-
if (typeof
|
|
116
|
-
|
|
117
|
-
|
|
210
|
+
if (typeof data !== "object" || data === null) {
|
|
211
|
+
const root = targets.find((t) => t.path.length === 0);
|
|
212
|
+
if (root) return coerceValue(data, root.type);
|
|
213
|
+
return data;
|
|
118
214
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
215
|
+
const sorted = [...targets].sort((a, b) => a.path.length - b.path.length);
|
|
216
|
+
const updateAtPath = (current, path, fn) => {
|
|
217
|
+
if (path.length === 0) return fn(current);
|
|
218
|
+
const [key, ...rest] = path;
|
|
219
|
+
if (key === "*") {
|
|
220
|
+
if (Array.isArray(current)) {
|
|
221
|
+
let changed = false;
|
|
222
|
+
const nextArr = current.map((item) => {
|
|
223
|
+
const nextVal = updateAtPath(item, rest, fn);
|
|
224
|
+
if (nextVal !== item) changed = true;
|
|
225
|
+
return nextVal;
|
|
127
226
|
});
|
|
128
|
-
return
|
|
227
|
+
return changed ? nextArr : current;
|
|
129
228
|
}
|
|
130
|
-
return
|
|
229
|
+
return current;
|
|
131
230
|
}
|
|
132
|
-
if (!
|
|
133
|
-
if (Array.isArray(
|
|
134
|
-
|
|
135
|
-
if (!Number.isNaN(
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
231
|
+
if (!current || typeof current !== "object") return current;
|
|
232
|
+
if (Array.isArray(current)) {
|
|
233
|
+
const index = Number(key);
|
|
234
|
+
if (!Number.isNaN(index) && index >= 0 && index < current.length) {
|
|
235
|
+
const nextVal = updateAtPath(current[index], rest, fn);
|
|
236
|
+
if (nextVal !== current[index]) {
|
|
237
|
+
const copy = [...current];
|
|
238
|
+
copy[index] = nextVal;
|
|
239
|
+
return copy;
|
|
140
240
|
}
|
|
141
241
|
}
|
|
142
|
-
return
|
|
242
|
+
return current;
|
|
143
243
|
}
|
|
144
|
-
if (Object.hasOwn(
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
...
|
|
148
|
-
[
|
|
244
|
+
if (Object.hasOwn(current, key)) {
|
|
245
|
+
const nextVal = updateAtPath(current[key], rest, fn);
|
|
246
|
+
if (nextVal !== current[key]) return {
|
|
247
|
+
...current,
|
|
248
|
+
[key]: nextVal
|
|
149
249
|
};
|
|
150
250
|
}
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
251
|
+
return current;
|
|
252
|
+
};
|
|
253
|
+
let result = data;
|
|
254
|
+
for (const t of sorted) if (t.path.length > 0) result = updateAtPath(result, t.path, (val) => coerceValue(val, t.type));
|
|
255
|
+
return result;
|
|
155
256
|
};
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Prepare an environment record by optionally stripping empty strings and applying coercion.
|
|
259
|
+
*
|
|
260
|
+
* @param env The raw environment variables
|
|
261
|
+
* @param emptyAsUndefined Whether to strip empty string values before processing
|
|
262
|
+
* @param arrayFormat The format to use for array coercion
|
|
263
|
+
* @param getSchema Optional callback that returns a JSON Schema and whether it exists,
|
|
264
|
+
* used to determine coercion targets. When omitted, no coercion is performed.
|
|
265
|
+
* @returns The processed environment, the coerced environment, and any missing schema keys
|
|
266
|
+
*/
|
|
267
|
+
function coerceEnvironment(env, emptyAsUndefined, arrayFormat, getSchema) {
|
|
268
|
+
const processedEnv = emptyAsUndefined ? stripEmptyStrings(env) : env;
|
|
269
|
+
let coercedEnv = { ...processedEnv };
|
|
270
|
+
const missingKeys = [];
|
|
271
|
+
if (getSchema) {
|
|
272
|
+
const result = getSchema();
|
|
273
|
+
missingKeys.push(...result.missingKeys || []);
|
|
274
|
+
if (result.hasSchema) coercedEnv = applyCoercion(coercedEnv, findCoercionPaths(result.schema), { arrayFormat });
|
|
161
275
|
}
|
|
162
276
|
return {
|
|
163
|
-
processedEnv
|
|
164
|
-
coercedEnv
|
|
165
|
-
missingKeys
|
|
277
|
+
processedEnv,
|
|
278
|
+
coercedEnv,
|
|
279
|
+
missingKeys
|
|
166
280
|
};
|
|
167
281
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return
|
|
178
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Indent a string by a given amount
|
|
284
|
+
* @param str - The string to indent
|
|
285
|
+
* @param amt - The amount to indent by, defaults to 2
|
|
286
|
+
* @param options - {@link IndentOptions}
|
|
287
|
+
* @returns The indented string
|
|
288
|
+
*/
|
|
289
|
+
const indent = (str, amt = 2, { dontDetectNewlines = false } = {}) => {
|
|
290
|
+
if (!dontDetectNewlines) return str.split("\n").map((line) => `${" ".repeat(amt)}${line}`).join("\n");
|
|
291
|
+
return `${" ".repeat(amt)}${str}`;
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Cross-platform text styling utility
|
|
295
|
+
* Uses ANSI colors in Node environments, plain text in browsers
|
|
296
|
+
* Respects NO_COLOR, CI environment variables, and TTY detection
|
|
297
|
+
*/
|
|
298
|
+
const colors = {
|
|
299
|
+
red: "\x1B[31m",
|
|
300
|
+
yellow: "\x1B[33m",
|
|
301
|
+
cyan: "\x1B[36m",
|
|
302
|
+
reset: "\x1B[0m"
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Check if we're in a Node environment (not browser)
|
|
306
|
+
* Checked dynamically to allow for testing with mocked globals
|
|
307
|
+
*/
|
|
308
|
+
const isNode = () => typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
309
|
+
/**
|
|
310
|
+
* Check if colors should be disabled based on environment
|
|
311
|
+
* Respects NO_COLOR, CI environment variables, and TTY detection
|
|
312
|
+
*/
|
|
313
|
+
const shouldDisableColors = () => {
|
|
314
|
+
if (!isNode()) return true;
|
|
315
|
+
if (process.env.NO_COLOR !== void 0) return true;
|
|
316
|
+
if (process.env.CI !== void 0) return true;
|
|
317
|
+
if (process.stdout && !process.stdout.isTTY) return true;
|
|
318
|
+
return false;
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* Style text with color. Uses ANSI codes in Node, plain text in browsers.
|
|
322
|
+
* @param color - The color to apply
|
|
323
|
+
* @param text - The text to style
|
|
324
|
+
* @returns Styled text in Node (if colors enabled), plain text otherwise
|
|
325
|
+
*/
|
|
326
|
+
const styleText = (color, text) => {
|
|
327
|
+
if (isNode() && !shouldDisableColors()) return `${colors[color]}${text}${colors.reset}`;
|
|
328
|
+
return text;
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* Format a list of normalized environment issues into a single styled string.
|
|
332
|
+
*
|
|
333
|
+
* @param issues - The array of normalized issues to format
|
|
334
|
+
* @returns The formatted and styled error report string
|
|
335
|
+
*/
|
|
336
|
+
function formatIssues(issues) {
|
|
337
|
+
return issues.map((issue) => {
|
|
338
|
+
return `${styleText("yellow", issue.path)} ${issue.message.trimStart()}`;
|
|
339
|
+
}).join("\n");
|
|
179
340
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Error thrown when environment variable validation fails.
|
|
343
|
+
*
|
|
344
|
+
* This error extends the native `Error` class and provides formatted error messages
|
|
345
|
+
* that clearly indicate which environment variables are invalid and why.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* try {
|
|
350
|
+
* const env = arkenv({
|
|
351
|
+
* PORT: 'number.port',
|
|
352
|
+
* HOST: 'string.host',
|
|
353
|
+
* });
|
|
354
|
+
* } catch (error) {
|
|
355
|
+
* if (error instanceof ArkEnvError) {
|
|
356
|
+
* console.error('Environment validation failed:', error.message);
|
|
357
|
+
* }
|
|
358
|
+
* }
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
var ArkEnvError = class extends Error {
|
|
362
|
+
constructor(issues, message = "Errors found while validating environment variables") {
|
|
363
|
+
const formattedIssues = formatIssues(issues);
|
|
364
|
+
super(`${styleText("red", message)}\n${indent(formattedIssues)}\n`);
|
|
365
|
+
this.name = ARKENV_ERROR_NAME;
|
|
366
|
+
this.issues = issues;
|
|
184
367
|
}
|
|
185
368
|
};
|
|
186
|
-
Object.defineProperty(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
369
|
+
Object.defineProperty(ArkEnvError, "name", { value: ARKENV_ERROR_NAME });
|
|
370
|
+
/**
|
|
371
|
+
* Regex pattern matching sensitive environment variable names.
|
|
372
|
+
*
|
|
373
|
+
* Matches keywords commonly associated with secrets (e.g. secret, key, token,
|
|
374
|
+
* password, pass, auth, jwt, cert, credential, db_url). Excludes public keys
|
|
375
|
+
* via the `shouldRedact` helper.
|
|
376
|
+
*
|
|
377
|
+
* @see {@link shouldRedact}
|
|
378
|
+
*/
|
|
379
|
+
const SENSITIVE_PATTERN = /secret|(_|^)key(_|$)|token|(_|^)password(_|$)|(_|^)pass(_|$)|(_|^)auth(_|$)|jwt|cert|credential|database_url|db_url/i;
|
|
380
|
+
/**
|
|
381
|
+
* Check if debug secrets mode is enabled.
|
|
382
|
+
*
|
|
383
|
+
* Debug secrets mode can be enabled programmatically via the `debugSecrets` config option,
|
|
384
|
+
* or globally by setting the `ARKENV_DEBUG_SECRETS` environment variable to `"true"` or `"1"`.
|
|
385
|
+
*
|
|
386
|
+
* @param configSecrets Programmatic override option for debugging secrets
|
|
387
|
+
* @returns A boolean indicating if debug secrets mode is active
|
|
388
|
+
*/
|
|
389
|
+
function isDebugSecrets(configSecrets) {
|
|
390
|
+
if (configSecrets !== void 0) return configSecrets;
|
|
391
|
+
if (typeof process === "undefined") return false;
|
|
392
|
+
const val = process.env.ARKENV_DEBUG_SECRETS;
|
|
393
|
+
return val === "true" || val === "1";
|
|
193
394
|
}
|
|
194
|
-
|
|
195
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Determine if an environment variable path matches sensitive keyword patterns.
|
|
397
|
+
*
|
|
398
|
+
* By default, environment variables that contain sensitive keywords (e.g. 'secret', 'key',
|
|
399
|
+
* 'token', 'password', 'auth', 'jwt', 'cert', 'credential', 'db_url') are flagged for redaction,
|
|
400
|
+
* unless they are explicitly marked as public (e.g., matching 'public').
|
|
401
|
+
*
|
|
402
|
+
* Redaction prevents sensitive values from being logged or printed to the terminal
|
|
403
|
+
* when environment validation fails.
|
|
404
|
+
*
|
|
405
|
+
* @param path The environment variable name/path under validation
|
|
406
|
+
* @returns A boolean indicating if the path is sensitive and should be redacted
|
|
407
|
+
*/
|
|
408
|
+
function shouldRedact(path) {
|
|
409
|
+
return SENSITIVE_PATTERN.test(path) && !/public/i.test(path);
|
|
196
410
|
}
|
|
197
|
-
|
|
411
|
+
/**
|
|
412
|
+
* Execute a parser function and return a SafeArkEnvResult.
|
|
413
|
+
*
|
|
414
|
+
* @param parseFn The function that parses the environment variables and might throw an ArkEnvError
|
|
415
|
+
* @returns A SafeArkEnvResult containing either the parsed data or the caught ArkEnvError
|
|
416
|
+
* @internal
|
|
417
|
+
*/
|
|
418
|
+
function safeExecute(parseFn) {
|
|
198
419
|
try {
|
|
199
420
|
return {
|
|
200
|
-
success:
|
|
201
|
-
data:
|
|
421
|
+
success: true,
|
|
422
|
+
data: parseFn()
|
|
202
423
|
};
|
|
203
|
-
} catch (
|
|
204
|
-
if (
|
|
205
|
-
success:
|
|
206
|
-
issues:
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if (error instanceof ArkEnvError) return {
|
|
426
|
+
success: false,
|
|
427
|
+
issues: error.issues
|
|
207
428
|
};
|
|
208
|
-
throw
|
|
429
|
+
throw error;
|
|
209
430
|
}
|
|
210
431
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
432
|
+
/**
|
|
433
|
+
* Build a normalized {@link EnvIssue}.
|
|
434
|
+
*
|
|
435
|
+
* @param path The dot-separated property path/name of the environment variable
|
|
436
|
+
* @param message The descriptive, user-friendly error message
|
|
437
|
+
* @param code The normalized classification code for the issue
|
|
438
|
+
* @param meta Additional validation metadata and engine codes
|
|
439
|
+
* @param expected The expected type or value shape description
|
|
440
|
+
* @param received The raw value received (redacted in string formatting if sensitive)
|
|
441
|
+
* @returns A fully populated EnvIssue
|
|
442
|
+
* @internal
|
|
443
|
+
*/
|
|
444
|
+
function buildEnvIssue(path, message, code, meta, expected, received) {
|
|
445
|
+
const issue = {
|
|
446
|
+
path,
|
|
447
|
+
message,
|
|
448
|
+
code,
|
|
449
|
+
meta: meta ?? {}
|
|
217
450
|
};
|
|
218
|
-
|
|
451
|
+
if (expected) issue.expected = expected;
|
|
452
|
+
if (received !== void 0) issue.received = received;
|
|
453
|
+
return issue;
|
|
219
454
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Extract the keys from a schema definition.
|
|
457
|
+
* Supports plain objects, ArkType schemas, and Standard Schema validators.
|
|
458
|
+
*
|
|
459
|
+
* @param schema The schema definition to extract keys from
|
|
460
|
+
* @returns An array of extracted key names
|
|
461
|
+
*/
|
|
462
|
+
function getSchemaKeys(schema) {
|
|
463
|
+
if (!schema || typeof schema !== "object" && typeof schema !== "function") return [];
|
|
464
|
+
if (schema.json && typeof schema.json === "object" && schema.json.domain === "object") {
|
|
465
|
+
const keys = [];
|
|
466
|
+
if (Array.isArray(schema.json.required)) {
|
|
467
|
+
for (const r of schema.json.required) if (r && typeof r === "object" && "key" in r) keys.push(r.key);
|
|
468
|
+
}
|
|
469
|
+
if (Array.isArray(schema.json.optional)) {
|
|
470
|
+
for (const o of schema.json.optional) if (o && typeof o === "object" && "key" in o) keys.push(o.key);
|
|
471
|
+
}
|
|
472
|
+
return keys;
|
|
227
473
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
474
|
+
const std = schema["~standard"];
|
|
475
|
+
const jsonSchemaInput = typeof std?.jsonSchema?.input === "function" && std.jsonSchema.input || typeof schema.jsonSchema?.input === "function" && schema.jsonSchema.input;
|
|
476
|
+
if (jsonSchemaInput) try {
|
|
477
|
+
const json = jsonSchemaInput({ target: "draft-07" });
|
|
478
|
+
if (json && typeof json === "object" && json.properties) return Object.keys(json.properties);
|
|
232
479
|
} catch {}
|
|
233
|
-
if (typeof
|
|
234
|
-
|
|
235
|
-
if (
|
|
480
|
+
if (typeof schema.toJSONSchema === "function") try {
|
|
481
|
+
const json = schema.toJSONSchema();
|
|
482
|
+
if (json && typeof json === "object" && json.properties) return Object.keys(json.properties);
|
|
236
483
|
} catch {}
|
|
237
|
-
if (typeof
|
|
238
|
-
|
|
239
|
-
if (
|
|
484
|
+
if (typeof schema.toStandardJSONSchema?.v1 === "function") try {
|
|
485
|
+
const json = schema.toStandardJSONSchema.v1();
|
|
486
|
+
if (json && typeof json === "object" && json.properties) return Object.keys(json.properties);
|
|
240
487
|
} catch {}
|
|
241
|
-
return Object.keys(
|
|
488
|
+
return Object.keys(schema);
|
|
242
489
|
}
|
|
243
490
|
|
|
244
491
|
//#endregion
|
|
@@ -296,9 +543,9 @@ function redactMessageWasValue(message, path, debugSecrets) {
|
|
|
296
543
|
const valueMatch = message.match(/\(was (.*)\)/);
|
|
297
544
|
if (!valueMatch?.[1]) return message;
|
|
298
545
|
const value = valueMatch[1];
|
|
299
|
-
const displayedValue = !
|
|
546
|
+
const displayedValue = !isDebugSecrets(debugSecrets) && shouldRedact(path) ? "[REDACTED]" : value;
|
|
300
547
|
if (displayedValue.includes("\x1B[")) return message;
|
|
301
|
-
return message.replace(`(was ${value})`, `(was ${
|
|
548
|
+
return message.replace(`(was ${value})`, `(was ${styleText("cyan", displayedValue)})`);
|
|
302
549
|
}
|
|
303
550
|
/**
|
|
304
551
|
* Convert ArkType's `ArkErrors` (keyed by path) into a flat `EnvIssue[]`
|
|
@@ -323,7 +570,7 @@ function arkErrorsToIssues(errors, config) {
|
|
|
323
570
|
message = redactMessageWasValue(message, path, config?.debugSecrets);
|
|
324
571
|
const code = mapArkTypeCode(error.code);
|
|
325
572
|
const meta = { ...getArkTypeMeta(error) };
|
|
326
|
-
return
|
|
573
|
+
return buildEnvIssue(path, message, code, meta, error.expected, error.code === "required" ? void 0 : error.data);
|
|
327
574
|
});
|
|
328
575
|
}
|
|
329
576
|
/**
|
|
@@ -349,22 +596,22 @@ function arkErrorsToIssues(errors, config) {
|
|
|
349
596
|
*/
|
|
350
597
|
function parse(def, config) {
|
|
351
598
|
const { env = process.env, coerce: shouldCoerce = true, onUndeclaredKey = "delete", arrayFormat = "comma", emptyAsUndefined = false } = config;
|
|
352
|
-
const schemaWithKeys = (typeof def === "function" && "assert" in def ? def : i
|
|
353
|
-
const { coercedEnv } =
|
|
599
|
+
const schemaWithKeys = (typeof def === "function" && "assert" in def ? def : i.type.raw(def)).onUndeclaredKey(onUndeclaredKey);
|
|
600
|
+
const { coercedEnv } = coerceEnvironment(env, emptyAsUndefined, arrayFormat, shouldCoerce ? () => {
|
|
354
601
|
return {
|
|
355
602
|
schema: schemaWithKeys.in.toJsonSchema({ fallback: (ctx) => ctx.base }),
|
|
356
603
|
hasSchema: true
|
|
357
604
|
};
|
|
358
605
|
} : void 0);
|
|
359
606
|
const validatedEnv = schemaWithKeys(coercedEnv);
|
|
360
|
-
if (validatedEnv instanceof arktype.ArkErrors) throw new
|
|
607
|
+
if (validatedEnv instanceof arktype.ArkErrors) throw new ArkEnvError(arkErrorsToIssues(validatedEnv, config));
|
|
361
608
|
return validatedEnv;
|
|
362
609
|
}
|
|
363
610
|
|
|
364
611
|
//#endregion
|
|
365
612
|
//#region src/arkenv.ts
|
|
366
613
|
function arkenv(def, config = {}) {
|
|
367
|
-
if (config.safe) return
|
|
614
|
+
if (config.safe) return safeExecute(() => parse(def, config));
|
|
368
615
|
return parse(def, config);
|
|
369
616
|
}
|
|
370
617
|
|
|
@@ -379,20 +626,15 @@ function arkenv(def, config = {}) {
|
|
|
379
626
|
* See ArkType's docs for the full API:
|
|
380
627
|
* https://arktype.io/docs/type-api
|
|
381
628
|
*/
|
|
382
|
-
const type = i
|
|
383
|
-
/**
|
|
384
|
-
* ArkEnv's main export, an alias for {@link arkenv}
|
|
385
|
-
*
|
|
386
|
-
* {@link https://arkenv.js.org | ArkEnv} is a typesafe environment variables validator from editor to runtime.
|
|
387
|
-
*/
|
|
629
|
+
const type = i.type;
|
|
388
630
|
var src_default = arkenv;
|
|
389
631
|
|
|
390
632
|
//#endregion
|
|
391
|
-
exports.ArkEnvError =
|
|
633
|
+
exports.ArkEnvError = ArkEnvError;
|
|
392
634
|
exports.arkenv = arkenv;
|
|
393
635
|
exports.default = src_default;
|
|
394
|
-
exports.formatIssues =
|
|
395
|
-
exports.getSchemaKeys =
|
|
636
|
+
exports.formatIssues = formatIssues;
|
|
637
|
+
exports.getSchemaKeys = getSchemaKeys;
|
|
396
638
|
exports.type = type;
|
|
397
639
|
|
|
398
640
|
// CJS Interop Shim
|