Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
427ced6
fix: tackle issue #709
yamcodes Jun 3, 2026
ca7c067
fix: resolve biome formatting and sync examples issues
yamcodes Jun 4, 2026
df34eed
Merge branch 'dev' into 709-unify-error-normalization-and-formatting
yamcodes Jun 5, 2026
94f275f
fix: address PR review comments on error formatters and sensitive key…
yamcodes Jun 5, 2026
083837c
fix: resolve size-limit failures by splitting redact utilities and op…
yamcodes Jun 5, 2026
b2b2e85
fix: resolve exactOptionalPropertyTypes errors in parse-standard.ts
yamcodes Jun 5, 2026
c0a5415
Merge branch 'v1' into 709-unify-error-normalization-and-formatting
yamcodes Jun 5, 2026
99c91e5
Merge branch 'v1' into 709-unify-error-normalization-and-formatting
yamcodes Jun 19, 2026
2321b1c
refactor: reorder internal imports and exports for improved consistency
yamcodes Jun 19, 2026
6a8da0c
fix: validate rawVal type in parse-standard and update bundle size li…
yamcodes Jun 19, 2026
dd03fe5
refactor: extract getProp helper and add code-review skill definition
yamcodes Jun 19, 2026
160e78f
refactor: centralize error mapping and metadata extraction for ArkTyp…
yamcodes Jun 19, 2026
f187548
fix(review): remove test file .ts extensions, restructure changeset, …
yamcodes Jun 19, 2026
2aa8a93
docs(skill): clarify that gh pr view completely omits inline code rev…
yamcodes Jun 19, 2026
52ff00c
refactor: remove redundant index file references from import paths ac…
yamcodes Jun 19, 2026
e51c73b
docs(changeset): elaborate on breaking change error message details
yamcodes Jun 19, 2026
0658e96
refactor: migrate root entrypoint imports from @/ to @ and update tsc…
yamcodes Jun 19, 2026
9b4eb34
refactor: use @ instead of @/index in dynamic import in isolation test
yamcodes Jun 19, 2026
fe83263
refactor: resolve second round of PR comments on error formatting and…
yamcodes Jun 19, 2026
40432cf
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 19, 2026
df87424
refactor: use imperative verb form and remove implementation details …
yamcodes Jun 19, 2026
e6ffea9
refactor: consolidate environment coercion logic and standardize path…
yamcodes Jun 19, 2026
4539b0a
refactor: replace Record<string, unknown> with Dict<string> in enviro…
yamcodes Jun 19, 2026
c90f532
refactor: move coerceEnvironment to dedicated module and update error…
yamcodes Jun 19, 2026
ad54a52
feat: upgrade arkenv to major version and document breaking API chang…
yamcodes Jun 19, 2026
53ba7dc
feat: import Dict type from repo types for shared coercion utilities
yamcodes Jun 20, 2026
c3fc46a
chore: add job execution logs and update package dependencies
yamcodes Jun 20, 2026
979c137
docs(review): resolve PR #1157 review comments, document safeArkEnv
yamcodes Jun 20, 2026
5d4e876
docs: document architectural design decisions regarding module isolat…
yamcodes Jun 20, 2026
aefb0d7
feat: add documentation for hiding internal comments from .d.ts files…
yamcodes Jun 20, 2026
63fa480
docs: address review comments
yamcodes Jun 21, 2026
101fbc9
refactor: update safeArkEnv return type to provide issues directly in…
yamcodes Jun 21, 2026
da4c544
refactor: replace safeArkEnv utility with a safe option in the arkenv…
yamcodes Jun 21, 2026
f922185
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 21, 2026
16e4d27
feat: add { safe: true } configuration option to arkenv for non-throw…
yamcodes Jun 21, 2026
6737534
refactor: remove engine-specific metadata from EnvIssue and deprecate…
yamcodes Jun 21, 2026
5066f2a
style: fix minor whitespace inconsistencies and clean up code formatting
yamcodes Jun 21, 2026
de18bbc
feat: disable strict validation by setting safe: false in plugin conf…
yamcodes Jun 21, 2026
43c7858
refactor: omit safe property from ArkEnvConfig in vite and bun plugin…
yamcodes Jun 21, 2026
3dd42d7
docs: clarify that the safe option is unsupported in bun and vite plu…
yamcodes Jun 21, 2026
ceb0496
docs: document the safe option and its limitations in framework integ…
yamcodes Jun 21, 2026
10a2103
refactor: enforce strict type safety for error code mappings using sa…
yamcodes Jun 21, 2026
befc6c9
fix: ensure type safety when accessing error maps by using the 'in' o…
yamcodes Jun 21, 2026
43206f2
fix: update error code mappings to support comprehensive ArkType vali…
yamcodes Jun 21, 2026
0ca680b
docs: remove safe parsing documentation from quickstart guide
yamcodes Jun 21, 2026
c9532d0
refactor: rename executeSafe to safeExecute for consistent naming con…
yamcodes Jun 21, 2026
11999c9
feat: remove safe option from integrations and enforce safe: false in…
yamcodes Jun 21, 2026
df221c2
chore: ignore job_logs.txt and clear existing log entries
yamcodes Jun 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/unified-error-normalization.md
Comment thread
yamcodes marked this conversation as resolved.
Comment thread
yamcodes marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"arkenv": minor
Comment thread
yamcodes marked this conversation as resolved.
Outdated
---

#### Refactor error system to use normalized `EnvIssue` and add `safeCreateEnv` API

**BREAKING CHANGE**: Unify validation error formatting across all validation engines (ArkType and Standard Schema). The exact text format of the `message` property on thrown `ArkEnvError` instances has changed. Check and update any test suites or CI/CD pipelines that assert on the exact error message text.

- Implement `EnvIssue` type and attach the full list of errors to `ArkEnvError.issues` for programmatic access.
- Add `safeCreateEnv` API to both `arkenv` and `arkenv/standard` entries for non-throwing validation in Server Actions or Next.js components.
- Standardize Standard Schema validation errors to look and act like ArkType validation errors, resolving received values and flattening paths.
- Redact credentials and sensitive environment variables by default in stringified error reports.
- Support `debugSecrets` configuration and `process.env.ARKENV_DEBUG_SECRETS=true` to temporarily bypass redaction.
Comment thread
yamcodes marked this conversation as resolved.
Outdated
4 changes: 2 additions & 2 deletions packages/arkenv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
{
"name": "arkenv",
"path": "dist/index.mjs",
"limit": "2 kB",
"limit": "2.3 kB",
"import": "*",
"ignore": [
"arktype"
Expand All @@ -97,7 +97,7 @@
{
"name": "arkenv/standard",
"path": "dist/standard.mjs",
"limit": "1.1 kB",
"limit": "2.1 kB",
"import": "*"
},
{
Expand Down
81 changes: 71 additions & 10 deletions packages/arkenv/src/arktype/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import { $ } from "@repo/scope";
import type { SchemaShape } from "@repo/types";
import type { distill } from "arktype";
import { ArkErrors } from "arktype";
import { ArkEnvError, type ValidationIssue } from "../core";
import type { ArkEnvConfig, EnvSchema } from "../create-env";
import {
ArkEnvError,
type EnvIssue,
type EnvIssueCode,
type EnvIssueMeta,
} from "../core.ts";
import type { ArkEnvConfig, EnvSchema } from "../create-env.ts";
import { shouldRedact } from "../utils/redact.ts";
import { styleText } from "../utils/style-text.ts";
import { coerce } from "./coercion/coerce";
import { coerce } from "./coercion/coerce.ts";

/**
* Re-export of ArkType's `distill` utilities.
Expand All @@ -19,14 +25,17 @@ import { coerce } from "./coercion/coerce";
export type { distill };

/**
* Converts ArkType's `ArkErrors` (keyed by path) into a flat `ValidationIssue[]`
* Converts ArkType's `ArkErrors` (keyed by path) into a flat `EnvIssue[]`
Comment thread
yamcodes marked this conversation as resolved.
Outdated
* suitable for `ArkEnvError`. Strips leading path references from messages to
* avoid duplication when `formatInternalErrors` prepends the styled path, and
* avoid duplication when `formatIssues` prepends the styled path, and
* applies cyan styling to inline "(was …)" values.
*
* @internal
*/
function arkErrorsToIssues(errors: ArkErrors): ValidationIssue[] {
function arkErrorsToIssues(
errors: ArkErrors,
config?: ArkEnvConfig,
): EnvIssue[] {
return Object.entries(errors.byPath).map(([path, error]) => {
let message = error.message;

Expand All @@ -43,19 +52,71 @@ function arkErrorsToIssues(errors: ArkErrors): ValidationIssue[] {
message = rest.trimStart();
}

// Check for redaction
const debugSecrets =
config?.debugSecrets ??
(typeof process !== "undefined" &&
(process.env.ARKENV_DEBUG_SECRETS === "true" ||
process.env.ARKENV_DEBUG_SECRETS === "1"));
Comment thread
yamcodes marked this conversation as resolved.
Outdated
const isSensitive = shouldRedact(path);

// Style (was ...) inline values
const valueMatch = message.match(/\(was (.*)\)/);
if (valueMatch?.[1]) {
const value = valueMatch[1];
if (!value.includes("\x1b[")) {
const displayedValue =
!debugSecrets && isSensitive ? "[REDACTED]" : value;
if (!displayedValue.includes("\x1b[")) {
message = message.replace(
`(was ${value})`,
`(was ${styleText("cyan", value)})`,
`(was ${styleText("cyan", displayedValue)})`,
);
}
}

return { path, message };
// Map code
let code: EnvIssueCode = "INVALID_TYPE";
if (error.code === "required") {
code = "MISSING_VARIABLE";
} else if (error.code === "pattern") {
code = "PATTERN_MISMATCH";
} else if (["min", "minLength"].includes(error.code)) {
code = "VALUE_TOO_SMALL";
} else if (["max", "maxLength"].includes(error.code)) {
code = "VALUE_TOO_LARGE";
} else if (
["divisor", "index", "sequence", "intersection", "union"].includes(
error.code,
)
) {
code = "INVALID_TYPE";
} else {
code = "INVALID_FORMAT";
}
Comment thread
yamcodes marked this conversation as resolved.
Outdated

// Safe meta extraction
const meta: EnvIssueMeta = {
engine: "arktype",
engineCode: error.code,
};
const errObj = error as any;
if (errObj.min !== undefined && typeof errObj.min === "number") {
meta.min = errObj.min;
} else if (errObj.rule !== undefined && typeof errObj.rule === "number") {
meta.min = errObj.rule;
}
if (errObj.max !== undefined && typeof errObj.max === "number") {
meta.max = errObj.max;
}
Comment thread
yamcodes marked this conversation as resolved.
Outdated

return {
path,
message,
code,
expected: error.expected,
received: error.code === "required" ? undefined : error.data,
meta,
};
});
}

Expand Down Expand Up @@ -105,7 +166,7 @@ export function parse<const T extends SchemaShape>(

// In ArkType 2.x, calling a type as a function returns the validated data or ArkErrors
if (validatedEnv instanceof ArkErrors) {
throw new ArkEnvError(arkErrorsToIssues(validatedEnv));
throw new ArkEnvError(arkErrorsToIssues(validatedEnv, config));
}

return validatedEnv;
Expand Down
70 changes: 57 additions & 13 deletions packages/arkenv/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,63 @@
import { indent } from "./utils/indent.ts";
import { styleText } from "./utils/style-text.ts";

/**
* A single validation issue produced during environment variable parsing.
* Used by {@link ArkEnvError} to report which key failed and why.
*/
export type ValidationIssue = {
export type EnvIssueCode =
Comment thread
yamcodes marked this conversation as resolved.
| "MISSING_VARIABLE"
| "INVALID_TYPE"
| "VALUE_TOO_SMALL"
| "VALUE_TOO_LARGE"
| "PATTERN_MISMATCH"
| "INVALID_FORMAT"
| "UNDECLARED_KEY"
| "INVALID_SCHEMA"
| "CUSTOM";
Comment thread
yamcodes marked this conversation as resolved.

export type EnvIssueMeta = {
min?: number;
max?: number;
validation?: string;
constraint?: string;
engineCode?: string;
engine: "arktype" | "zod" | "valibot" | "unknown";
Comment thread
yamcodes marked this conversation as resolved.
Outdated
traversalError?: string;
};

export type EnvIssue = {
path: string;
message: string;
code: EnvIssueCode;
expected?: string;
received?: unknown;
meta?: EnvIssueMeta;
};

export const formatInternalErrors = (errors: ValidationIssue[]): string =>
errors
.map(
(error) =>
`${styleText("yellow", error.path)} ${error.message.trimStart()}`,
)
/**
* @deprecated Use EnvIssue instead
*/
export type ValidationIssue = EnvIssue;

export function formatIssues(issues: EnvIssue[]): string {
return issues
.map((issue) => {
const pathStr = styleText("yellow", issue.path);
const messageStr = issue.message.trimStart();
return `${pathStr} ${messageStr}`;
})
.join("\n");
}

export function formatError(error: ArkEnvError | EnvIssue[]): string {
if (Array.isArray(error)) {
return formatIssues(error);
}
return formatIssues(error.issues);
}
Comment thread
yamcodes marked this conversation as resolved.
Outdated

/**
* @deprecated Use formatIssues instead
*/
export const formatInternalErrors = (errors: ValidationIssue[]): string =>
formatIssues(errors);
Comment thread
yamcodes marked this conversation as resolved.
Outdated

/**
* Error thrown when environment variable validation fails.
Expand All @@ -42,13 +83,16 @@ export const formatInternalErrors = (errors: ValidationIssue[]): string =>
* ```
*/
export class ArkEnvError extends Error {
readonly issues: EnvIssue[];

constructor(
errors: ValidationIssue[],
issues: EnvIssue[],
message = "Errors found while validating environment variables",
) {
const formattedErrors = formatInternalErrors(errors);
const formattedErrors = formatIssues(issues);
Comment thread
yamcodes marked this conversation as resolved.
Outdated
super(`${styleText("red", message)}\n${indent(formattedErrors)}\n`);
this.name = "ArkEnvError";
this.issues = issues;
}
}

Expand Down
52 changes: 47 additions & 5 deletions packages/arkenv/src/create-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type {
SchemaShape,
} from "@repo/types";
import type { type as at, distill } from "arktype";
import { parse } from "./arktype";
import type { ArkEnvError } from "./core";
import { parse } from "./arktype/index.ts";
import { ArkEnvError, type EnvIssue } from "./core.ts";

/**
* Declarative environment schema definition accepted by ArkEnv.
Expand Down Expand Up @@ -74,13 +74,20 @@ export type ArkEnvConfig = {
* @default "comma"
*/
arrayFormat?: "comma" | "json";

/**
* Whether to bypass secret redaction and print raw sensitive values during debugging.
* Defaults to checking `process.env.ARKENV_DEBUG_SECRETS === "true"` or `"1"`.
*/
debugSecrets?: boolean;
};

/**
* TODO: `SchemaShape` is basically `Record<string, unknown>`.
* If possible, find a better type than "const T extends Record<string, unknown>",
* and be as close as possible to the type accepted by ArkType's `type`.
* The result structure returned by safeCreateEnv.
*/
export type SafeCreateEnvResult<T> =
| { success: true; data: T }
| { success: false; error: string; issues: EnvIssue[] };

/**
* Utility to parse environment variables using ArkType or Standard Schema
Expand Down Expand Up @@ -108,3 +115,38 @@ export function createEnv<const T extends SchemaShape>(
// biome-ignore lint/suspicious/noExplicitAny: parse handles both EnvSchema<T> and CompiledEnvSchema at runtime
return parse(def as any, config);
}

/**
* Non-throwing utility to parse environment variables using ArkType or Standard Schema.
* Returns a serializable result object containing either the validated data or error issues.
*
* @param def - The schema definition
* @param config - The evaluation configuration
* @returns The SafeCreateEnvResult containing the data or plain error issues
*/
export function safeCreateEnv<const T extends SchemaShape>(
def: EnvSchema<T>,
config?: ArkEnvConfig,
): SafeCreateEnvResult<distill.Out<at.infer<T, $>>>;
export function safeCreateEnv<T extends CompiledEnvSchema>(
def: T,
config?: ArkEnvConfig,
): SafeCreateEnvResult<InferType<T>>;
export function safeCreateEnv<const T extends SchemaShape>(
def: EnvSchema<T> | CompiledEnvSchema,
config?: ArkEnvConfig,
): SafeCreateEnvResult<distill.Out<at.infer<T, $>> | InferType<typeof def>>;
export function safeCreateEnv<const T extends SchemaShape>(
def: EnvSchema<T> | CompiledEnvSchema,
config: ArkEnvConfig = {},
): SafeCreateEnvResult<distill.Out<at.infer<T, $>> | InferType<typeof def>> {
try {
const data = createEnv(def as any, config);
return { success: true, data };
} catch (error) {
if (error instanceof ArkEnvError) {
return { success: false, error: error.message, issues: error.issues };
}
throw error;
}
}
Loading
Loading