Files
2026-05-27 23:29:53 +01:00

9.4 KiB

Capability Spec: Env

Package: @suaveplan/env Version: 0.0.0 (genesis) Status: ADDED


ADDED Requirements

ENV-01 — Schema-first type-safe loading

createEnv(schema, options?) MUST accept an EnvSchema (a Record<string, AnyField>) and an optional CreateEnvOptions. It MUST run all configured loaders in order, merge their LoaderResult.values maps (later loaders win by default), resolve ${VAR} interpolation tokens, coerce each raw string through its field's coerce() method, run all registered validation rules, and return an EnvResult<S> whose type is fully inferred from the schema via InferEnv<S>. The returned object MUST be typed so that required fields are never undefined and optional fields may be undefined.

ENV-02 — Error mode contract

createEnv MUST support three ErrorMode values:

  • 'throw' (default): throw EnvValidationError or MissingEnvError on the first field failure.
  • 'collect': accumulate all field failures into CollectedErrors (a Record<string, string[]>) without throwing; return the partial result alongside the error map.
  • 'warn': log each failure via console.warn and return the partial result; do not throw.

ENV-03 — Loader contract

A Loader MUST implement { name: string; load(): LoaderResult }. LoaderResult MUST be { values: Record<string, string>; source: string }. createEnv MUST call each loader's load() in the order they appear in options.loaders. If no loaders are provided the raw values map is empty and only defaults and optional fields can produce output.

ENV-04 — objectLoader

objectLoader(obj, options?) MUST wrap any Record<string, string | undefined> as a Loader. ObjectLoaderOptions.source (optional) sets the LoaderResult.source label; defaults to 'object'. Values that are undefined MUST be omitted from LoaderResult.values.

ENV-05 — dotenvLoader and parseDotenv

parseDotenv(content) MUST be a pure function that parses a dotenv-format string into a Record<string, string>. It MUST handle: quoted values (single and double), inline comments (# …), blank lines, and = characters inside values. Malformed lines that cannot be parsed MUST cause a DotenvParseError (code ENV.PARSE_ERROR). dotenvLoader(options?) MUST read the file at options.path (default '.env'), call parseDotenv, and return a LoaderResult. If the file does not exist the loader MUST return an empty LoaderResult (no error); this allows optional .env files. DotenvLoaderOptions.override (boolean, default true) controls whether dotenv values overwrite values already present from earlier loaders.

ENV-06 — Interpolation

interpolate(template, values) MUST replace all ${VAR} tokens in template with the corresponding entry from values, leaving unresolved tokens as-is. hasInterpolation(value) MUST return true if and only if the string contains at least one ${VAR} token. extractVariables(template) MUST return all unique variable names referenced in the template. createEnv MUST expand interpolation on every raw value before coercion.

ENV-07 — BaseField builder contract

BaseField<T> MUST provide the following fluent builder methods, each returning this for chaining:

  • optional() — marks the field as not required; returns undefined when the key is absent and there is no default
  • default(v: T) — sets a default value used when the key is absent
  • sensitive() — marks the field so maskSensitive redacts its value in logs/output
  • describe(s: string) — attaches a human-readable description
  • rules(opts: RuleOptions) — appends a named custom validation rule

BaseField MUST declare abstract coerce(raw: string): T. Calling coerce with an uncoercible string MUST throw CoercionError (code ENV.INVALID_FORMAT).

ENV-08 — Field type coercion and rules

Each field type MUST coerce its raw string through the corresponding transform and enforce its built-in rules before any custom rules run:

  • StringField — identity coercion (trim); built-in rules: minLength, maxLength, pattern, nonempty
  • NumberField — parseFloat; rejects NaN; built-in rules: min, max, integer
  • BooleanField — maps TRUTHY_VALUES strings to true, FALSY_VALUES strings to false; any other value is a coercion error
  • EnumField<V> — validates raw string is a member of the declared EnumValues tuple
  • UrlField — validates via new URL(); optional protocols allowlist rule rejects disallowed schemes
  • EmailField — validates against EMAIL_PATTERN regex
  • PortField — coerces to integer; validates in [PORT_MIN, PORT_MAX]
  • JsonField<T> — JSON.parse; applies optional JsonValidator<T> for shape checking
  • ArrayField<T> — splits on separator (default DEFAULT_ARRAY_SEPARATOR = ','), trims each item, maps through itemField.coerce

ENV-09 — validate accumulates all errors

validate(input: ValidateInput) MUST run all rules in input.field._rules and collect every failure. The returned ValidationResult MUST be { ok: boolean; errors: Array<{ rule: string; message: string }> } where errors contains one entry per failing rule. ok MUST be true if and only if errors is empty.

ENV-10 — maskSensitive never leaks

maskSensitive(values, schema) MUST return a shallow copy of values where every key whose corresponding schema[key]._isSensitive is true has its value replaced with the literal string '***'. The original values object MUST NOT be mutated.

ENV-11 — Structured errors with ENV.* codes

All errors thrown by this package MUST extend the EnvError base (which extends SuaveplanError). Error codes MUST be registered via defineCodes in the ENV.* namespace:

Code Class Trigger
ENV.VALIDATION_FAILED EnvValidationError One or more field rules failed
ENV.MISSING_REQUIRED MissingEnvError Required field has no value and no default
ENV.INVALID_FORMAT CoercionError Raw string cannot be coerced to target type
ENV.PARSE_ERROR DotenvParseError Dotenv file or JSON value cannot be parsed

Every error instance MUST carry { key: string; raw?: string; source?: string } in its structured context.

ENV-12 — Zod schemas for runtime validation

The following Zod schemas MUST be exported from *.schemas.ts sibling files and used to validate inputs at public API boundaries:

  • FieldConfigSchema — validates FieldConfig<unknown>
  • ValidationResultSchema — validates ValidationResult
  • CreateEnvOptionsSchema — validates CreateEnvOptions
  • DotenvLoaderOptionsSchema — validates DotenvLoaderOptions
  • ObjectLoaderOptionsSchema — validates ObjectLoaderOptions

Invalid options passed to createEnv, dotenvLoader, or objectLoader MUST throw EnvValidationError with code ENV.VALIDATION_FAILED and a context that includes the Zod parse error details.

ENV-13 — Presets

The common preset fields (nodeEnvField, portField, hostField, logLevelField, debugField, appNameField, appVersionField, timezoneField, workersField) and the commonFields composite MUST be exported from src/presets/common/. The server preset fields (databaseUrlField, redisUrlField, sessionSecretField, corsOriginField, rateLimitMaxField, rateLimitWindowField, trustProxyField, smtpHostField, smtpPortField, smtpUserField, smtpPassField, smtpFromField) and the serverFields composite MUST be exported from src/presets/server/. All sensitive preset fields (database URL, Redis URL, session secret, SMTP password) MUST have _isSensitive = true.

ENV-14 — OTel instrumentation

createEnv MUST emit the following metrics via @opentelemetry/api getMeter:

  • Counter env.validation.total with label outcome: 'success' | 'error' — incremented once per createEnv invocation after all fields are processed
  • Counter env.validation.errors with label field: string — incremented once per field that fails validation or coercion
  • Histogram env.load.duration_ms — records the wall-clock duration of the full createEnv call

All instruments MUST be no-ops when no OTel SDK exporter is registered. The instrumentation MUST NOT throw or suppress errors from the calling code.

ENV-15 — sideEffects: false + coverage 100%

"sideEffects": false MUST be set in package.json and enforced by a tree-shake guard test in tests/. Vitest coverage thresholds MUST be set to 100/100/100/100 (statement / branch / function / line). No .skip or .only markers are permitted in src/.

ENV-16 — Property-based test contracts

The following fast-check properties MUST pass:

  • For all valid raw strings s, coerce(s) produces the same stable value on repeated calls (field coerce functions are pure)
  • For any schema with at least one sensitive field, maskSensitive(values, schema) MUST NOT contain the original raw value at any sensitive key
  • For any N failing rules, validate MUST return errors.length === N
  • parseDotenv(content) MUST be pure: same content input always produces an equal output map

ENV-17 — No browser or Node-specific dependencies

The package MUST run in Bun, Node, and browser environments. It MUST NOT import fs, path, process, or any Node-only built-in in any code path that does not go through dotenvLoader. dotenvLoader itself MAY use file-system access; all other modules MUST be environment-agnostic.