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