@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/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$1 = type$1(`0 <= number.integer <= 65535`), r$1 = type$1(`string.ip | 'localhost'`), i$1 = scope({
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$1
7
+ host: r
8
8
  }),
9
9
  number: type$1.module({
10
10
  ...type$1.keywords.number,
11
- port: n$1
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
- const e = (e) => {
18
- if (typeof e == `number` || typeof e != `string` || !e.trim()) return e;
19
- if (e.trim() === `NaN`) return NaN;
20
- let t = Number(e);
21
- return Number.isNaN(t) ? e : t;
22
- }, t = (e) => e === `true` ? !0 : e === `false` ? !1 : e, n = (e) => {
23
- if (typeof e != `string`) return e;
24
- let t = e.trim();
25
- if (t[0] !== `{` && t[0] !== `[`) return e;
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(t);
72
+ return JSON.parse(trimmed);
28
73
  } catch {
29
- return e;
74
+ return s;
30
75
  }
31
- }, r = (e) => {
32
- if (e instanceof Date || typeof e != `string` || !e.trim()) return e;
33
- let t = new Date(e);
34
- return Number.isNaN(t.getTime()) ? e : t;
35
- }, i = (e) => {
36
- let t = {};
37
- for (let n in e) {
38
- let r = e[n];
39
- r !== `` && (t[n] = r);
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 t;
42
- }, a = `*`, o = (e, t = []) => {
43
- let n = [];
44
- if (!e || typeof e != `object` || Array.isArray(e)) return n;
45
- let r = e;
46
- if (`const` in r) {
47
- let e = typeof r.const;
48
- (e === `number` || e === `boolean`) && n.push({
49
- path: [...t],
50
- type: `primitive`
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
- `enum` in r && Array.isArray(r.enum) && r.enum.some((e) => typeof e == `number` || typeof e == `boolean`) && n.push({
54
- path: [...t],
55
- type: `primitive`
56
- });
57
- let i = r.type;
58
- if (i === `number` || i === `integer` || i === `boolean`) n.push({
59
- path: [...t],
60
- type: `primitive`
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 (i === `string` && `format` in r && (r.format === `date-time` || r.format === `date`)) n.push({
63
- path: [...t],
64
- type: `date`
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 (i === `object`) {
67
- if (r.properties && Object.keys(r.properties).length > 0) {
68
- n.push({
69
- path: [...t],
70
- type: `object`
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 (let e in r.properties) n.push(...o(r.properties[e], [...t, e]));
152
+ for (const key in n.properties) results.push(...findCoercionPaths(n.properties[key], [...path, key]));
73
153
  }
74
- } else i === `array` && (n.push({
75
- path: [...t],
76
- type: `array`
77
- }), r.items && (Array.isArray(r.items) ? r.items.forEach((e, r) => {
78
- n.push(...o(e, [...t, String(r)]));
79
- }) : n.push(...o(r.items, [...t, `*`]))));
80
- for (let e of [
81
- `anyOf`,
82
- `allOf`,
83
- `oneOf`
84
- ]) if (r[e] && Array.isArray(r[e])) for (let i of r[e]) n.push(...o(i, t));
85
- let a = /* @__PURE__ */ new Set();
86
- return n.filter((e) => {
87
- let t = e.path.join(`/`) + `:` + e.type;
88
- return a.has(t) ? !1 : a.add(t);
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
- }, s = (i, a, o = {}) => {
91
- let { arrayFormat: s = `comma` } = o, c = (e) => {
92
- if (s === `json`) try {
93
- return JSON.parse(e);
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 e;
189
+ return val;
96
190
  }
97
- return e.trim() ? e.split(`,`).map((e) => e.trim()) : [];
98
- }, l = (i, a) => {
99
- if (a === `array` && typeof i == `string`) return c(i);
100
- if (a === `object` && typeof i == `string`) return n(i);
101
- if (a === `date` && typeof i == `string`) return r(i);
102
- if (a === `primitive`) {
103
- if (Array.isArray(i)) return i.map((n) => {
104
- if (typeof n != `string`) return n;
105
- let r = e(n);
106
- return typeof r == `number` ? r : t(n);
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 i != `string`) return i;
109
- let n = e(i);
110
- return typeof n == `number` ? n : t(i);
203
+ if (typeof val !== "string") return val;
204
+ const n = coerceNumber(val);
205
+ return typeof n === "number" ? n : coerceBoolean(val);
111
206
  }
112
- return i;
207
+ return val;
113
208
  };
114
- if (typeof i != `object` || !i) {
115
- let e = a.find((e) => e.path.length === 0);
116
- return e ? l(i, e.type) : i;
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
- let u = [...a].sort((e, t) => e.path.length - t.path.length), d = (e, t, n) => {
119
- if (t.length === 0) return n(e);
120
- let [r, ...i] = t;
121
- if (r === `*`) {
122
- if (Array.isArray(e)) {
123
- let t = !1, r = e.map((e) => {
124
- let r = d(e, i, n);
125
- return r !== e && (t = !0), r;
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 t ? r : e;
226
+ return changed ? nextArr : current;
128
227
  }
129
- return e;
228
+ return current;
130
229
  }
131
- if (!e || typeof e != `object`) return e;
132
- if (Array.isArray(e)) {
133
- let t = Number(r);
134
- if (!Number.isNaN(t) && t >= 0 && t < e.length) {
135
- let r = d(e[t], i, n);
136
- if (r !== e[t]) {
137
- let n = [...e];
138
- return n[t] = r, n;
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 e;
241
+ return current;
142
242
  }
143
- if (Object.hasOwn(e, r)) {
144
- let t = d(e[r], i, n);
145
- if (t !== e[r]) return {
146
- ...e,
147
- [r]: t
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 e;
151
- }, f = i;
152
- for (let e of u) e.path.length > 0 && (f = d(f, e.path, (t) => l(t, e.type)));
153
- return f;
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
- function c(e, t, n, r) {
156
- let a = t ? i(e) : e, c = { ...a }, l = [];
157
- if (r) {
158
- let e = r();
159
- l.push(...e.missingKeys || []), e.hasSchema && (c = s(c, o(e.schema), { arrayFormat: n }));
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: a,
163
- coercedEnv: c,
164
- missingKeys: l
276
+ processedEnv,
277
+ coercedEnv,
278
+ missingKeys
165
279
  };
166
280
  }
167
- const l = (e, t = 2, { dontDetectNewlines: n = !1 } = {}) => n ? `${` `.repeat(t)}${e}` : e.split(`
168
- `).map((e) => `${` `.repeat(t)}${e}`).join(`
169
- `), u = {
170
- red: `\x1B[31m`,
171
- yellow: `\x1B[33m`,
172
- cyan: `\x1B[36m`,
173
- reset: `\x1B[0m`
174
- }, d = () => typeof process < `u` && process.versions != null && process.versions.node != null, f = () => !!(!d() || process.env.NO_COLOR !== void 0 || process.env.CI !== void 0 || process.stdout && !process.stdout.isTTY), p = (e, t) => d() && !f() ? `${u[e]}${t}${u.reset}` : t;
175
- function m(e) {
176
- return e.map((e) => `${p(`yellow`, e.path)} ${e.message.trimStart()}`).join(`
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
- var h = class extends Error {
180
- constructor(e, t = `Errors found while validating environment variables`) {
181
- let n = m(e);
182
- super(`${p(`red`, t)}\n${l(n)}\n`), this.name = `ArkEnvError`, this.issues = e;
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(h, `name`, { value: `ArkEnvError` });
186
- const y = /secret|(_|^)key(_|$)|token|(_|^)password(_|$)|(_|^)pass(_|$)|(_|^)auth(_|$)|jwt|cert|credential|database_url|db_url/i;
187
- function b(e) {
188
- if (e !== void 0) return e;
189
- if (typeof process > `u`) return !1;
190
- let t = process.env.ARKENV_DEBUG_SECRETS;
191
- return t === `true` || t === `1`;
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
- function x(e) {
194
- return y.test(e) && !/public/i.test(e);
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
- function E(e) {
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: !0,
200
- data: e()
420
+ success: true,
421
+ data: parseFn()
201
422
  };
202
- } catch (e) {
203
- if (e instanceof h) return {
204
- success: !1,
205
- issues: e.issues
423
+ } catch (error) {
424
+ if (error instanceof ArkEnvError) return {
425
+ success: false,
426
+ issues: error.issues
206
427
  };
207
- throw e;
428
+ throw error;
208
429
  }
209
430
  }
210
- function D(e, t, n, r, i, a) {
211
- let o = {
212
- path: e,
213
- message: t,
214
- code: n,
215
- meta: r ?? {}
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
- return i && (o.expected = i), a !== void 0 && (o.received = a), o;
450
+ if (expected) issue.expected = expected;
451
+ if (received !== void 0) issue.received = received;
452
+ return issue;
218
453
  }
219
- function P(e) {
220
- if (!e || typeof e != `object` && typeof e != `function`) return [];
221
- if (e.json && typeof e.json == `object` && e.json.domain === `object`) {
222
- let t = [];
223
- if (Array.isArray(e.json.required)) for (let n of e.json.required) n && typeof n == `object` && `key` in n && t.push(n.key);
224
- if (Array.isArray(e.json.optional)) for (let n of e.json.optional) n && typeof n == `object` && `key` in n && t.push(n.key);
225
- return t;
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
- let t = e[`~standard`], n = typeof t?.jsonSchema?.input == `function` && t.jsonSchema.input || typeof e.jsonSchema?.input == `function` && e.jsonSchema.input;
228
- if (n) try {
229
- let e = n({ target: `draft-07` });
230
- if (e && typeof e == `object` && e.properties) return Object.keys(e.properties);
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 e.toJSONSchema == `function`) try {
233
- let t = e.toJSONSchema();
234
- if (t && typeof t == `object` && t.properties) return Object.keys(t.properties);
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 e.toStandardJSONSchema?.v1 == `function`) try {
237
- let t = e.toStandardJSONSchema.v1();
238
- if (t && typeof t == `object` && t.properties) return Object.keys(t.properties);
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(e);
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 = !b(debugSecrets) && x(path) ? "[REDACTED]" : value;
545
+ const displayedValue = !isDebugSecrets(debugSecrets) && shouldRedact(path) ? "[REDACTED]" : value;
299
546
  if (displayedValue.includes("\x1B[")) return message;
300
- return message.replace(`(was ${value})`, `(was ${p("cyan", displayedValue)})`);
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 D(path, message, code, meta, error.expected, error.code === "required" ? void 0 : error.data);
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$1.type.raw(def)).onUndeclaredKey(onUndeclaredKey);
352
- const { coercedEnv } = c(env, emptyAsUndefined, arrayFormat, shouldCoerce ? () => {
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 h(arkErrorsToIssues(validatedEnv, config));
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 E(() => parse(def, config));
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$1.type;
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 { h as ArkEnvError, arkenv, src_default as default, m as formatIssues, P as getSchemaKeys, type };
632
+ export { ArkEnvError, arkenv, src_default as default, formatIssues, getSchemaKeys, type };
391
633
 
392
634
  //# sourceMappingURL=index.mjs.map