Files
suaveplan/openspec/archive/2026-04-19-enterprise-env/proposal.md
2026-05-27 23:29:53 +01:00

10 KiB

OpenSpec Change: enterprise-env

  • Change ID: enterprise-env
  • Package: @suaveplan/env
  • Category: universal
  • Target path: packages/universal/env
  • Status: PROPOSED
  • Date: 2026-04-19
  • Reference spec: /mnt/s/web/suaveplan/enterprise/env.md

Summary

Greenfield implementation of @suaveplan/env for the genesis branch. The existing main-branch env package provides the baseline public-API surface to preserve; this change delivers enterprise-grade type-safe environment variable loading with schema-first validation, a rich set of typed field builders (string, number, boolean, enum, URL, email, port, JSON, array), pluggable loaders (dotenv file, plain object), common presets for server apps, structured errors with ENV.* codes, Zod schemas, OTel instrumentation, property-based tests via fast-check, and benchmarks.

Current-state audit (genesis)

The package does not exist on genesis yet — this is a fresh build.

Available workspace deps on genesis

Package Available
@suaveplan/error ✅
@suaveplan/types ✅
@suaveplan/logger ✅
@suaveplan/opentelemetry ❌ not yet built — use @opentelemetry/api directly
@suaveplan/schema-registry ❌ not yet built — export Zod schemas directly

External deps (via catalog):

  • zod → catalog:forms
  • @opentelemetry/api → catalog:observability
  • tslib → catalog:build

Dev deps (via catalog):

  • vitest, @vitest/coverage-istanbul → catalog:testing
  • fast-check → catalog:testing
  • mitata → catalog:testing

Preserved public API surface (from main branch)

Constants

  • DEFAULT_ARRAY_SEPARATOR — default delimiter used by ArrayField
  • EMAIL_PATTERN — regex for RFC-5322-compatible email validation
  • FALSY_VALUES — string values coerced to false by BooleanField
  • INTERPOLATION_PATTERN — regex matching ${VAR} tokens in raw values
  • PORT_MAX — upper bound for valid TCP port (65535)
  • PORT_MIN — lower bound for valid TCP port (1)
  • TRUTHY_VALUES — string values coerced to true by BooleanField

Core

  • createEnv(schema, options?) — loads env vars through configured loaders, validates all fields, collects or throws errors, returns typed record
  • CollectedErrors — map of field key → array of error messages produced during a collect-mode run
  • EnvResult<S> — the resolved typed record produced by createEnv
  • BaseField<T> — abstract base class for all field builders; holds rules, default, sensitive flag, description
  • FieldConfig<T> — runtime configuration snapshot extracted from any BaseField
  • RuleOptions — options accepted by BaseField.rules() for adding a named validation rule
  • maskSensitive(values, schema) — replaces sensitive field values with *** in the result map
  • validate(input) → ValidationResult — runs all rules for a single field, accumulating failures
  • ValidateInput — { key, raw, field, coerced? } fed into validate
  • ValidationResult — { ok, errors } where errors is an array of { rule, message } pairs

Types

  • AnyField<T, R> — minimal interface every field builder satisfies; used for schema maps
  • CreateEnvOptions — options accepted by createEnv: loaders, errorMode, signal?
  • EnvError — { key, message, value? } shape for a single validation failure
  • EnvSchema — Record<string, AnyField> used as the schema argument to createEnv
  • ErrorMode — 'throw' | 'collect' | 'warn'
  • InferEnv<S> — maps an EnvSchema to the resulting typed record type
  • InferFieldType<F> — extracts the value type from a field builder
  • Loader — { name: string; load(): LoaderResult } interface for env sources
  • LoaderResult — { values: Record<string, string>; source: string }
  • OptionalKeys<S> — keys whose fields may produce undefined
  • RequiredKeys<S> — keys whose fields always produce a value
  • Simplify<T> — intersection flattener utility type
  • ValidationRule<T> — { name, message, validate(value): boolean }

Fields

  • StringField / str — string field builder; rules: minLength, maxLength, pattern, nonempty
  • NumberField / num — number field; rules: min, max, integer
  • BooleanField / bool — boolean field; coerces TRUTHY_VALUES / FALSY_VALUES strings
  • EnumField<V> / enums — enum field locked to a declared EnumValues tuple
  • EnumValues — string-tuple type constraint for EnumField
  • UrlField / url — URL field; validates via URL constructor, optional protocol restriction
  • EmailField / email — email field; validates against EMAIL_PATTERN
  • PortField / port — port field; integer in [PORT_MIN, PORT_MAX]
  • JsonField<T> / json — JSON field; accepts a JsonValidator<T> for runtime shape checking
  • JsonValidator<T> — (parsed: unknown) => T used with JsonField
  • ArrayField<T> / array — array field; splits by DEFAULT_ARRAY_SEPARATOR (or custom), maps each item through an ItemField
  • ItemField<T> — per-element field descriptor for ArrayField

Loaders

  • dotenvLoader(options?) — reads a .env file from disk and returns a LoaderResult
  • parseDotenv(content) — pure function: parses a dotenv-format string into a Record<string, string>
  • DotenvLoaderOptions — { path?, encoding?, override? }
  • objectLoader(obj, options?) — wraps an in-memory object as a loader
  • ObjectLoaderOptions — { source? } (source label for diagnostics)

Presets — common

  • nodeEnvField — NODE_ENV enum field locked to 'development' | 'test' | 'production' | 'staging'
  • portField — PORT with default 3000
  • hostField — HOST with default '0.0.0.0'
  • logLevelField — LOG_LEVEL enum locked to standard logger levels
  • debugField — DEBUG boolean with default false
  • appNameField — APP_NAME required string
  • appVersionField — APP_VERSION string with default '0.0.0'
  • timezoneField — TZ string with default 'UTC'
  • workersField — WORKERS integer number
  • commonFields — composite preset: nodeEnvField + portField + hostField + logLevelField + debugField

Presets — server

  • databaseUrlField — DATABASE_URL sensitive required URL
  • redisUrlField — REDIS_URL sensitive URL
  • sessionSecretField — SESSION_SECRET sensitive required string with minLength: 32
  • corsOriginField — CORS_ORIGIN string or array
  • rateLimitMaxField — RATE_LIMIT_MAX integer number
  • rateLimitWindowField — RATE_LIMIT_WINDOW_MS integer number
  • trustProxyField — TRUST_PROXY boolean with default false
  • smtpHostField — SMTP_HOST string
  • smtpPortField — SMTP_PORT port field
  • smtpUserField — SMTP_USER string
  • smtpPassField — SMTP_PASS sensitive string
  • smtpFromField — SMTP_FROM email field
  • serverFields — composite: common server connection + SMTP fields

Transforms (coerce utilities)

  • coerceString(raw) — identity / trim
  • coerceNumber(raw) — parses float, rejects NaN
  • coerceInteger(raw) — parses integer, rejects non-integer
  • coerceBoolean(raw) — maps truthy/falsy string sets
  • coerceArray(raw, separator?) — splits and trims
  • coerceJson(raw) — JSON.parse with structured error on failure
  • CoerceFn<T> — (raw: string) => T
  • CoerceEntry<T> — { coerce: CoerceFn<T> } mixin used internally

Utilities

  • EnvValidationError extends SuaveplanError — thrown in 'throw' error mode
  • MissingEnvError extends SuaveplanError — thrown when a required key is absent
  • CoercionError extends SuaveplanError — thrown when coercion fails for a raw value
  • DotenvParseError extends SuaveplanError — thrown by parseDotenv on malformed input
  • createEnvError(key, message, value?) — factory for EnvError value objects
  • interpolate(template, values) — resolves ${VAR} tokens in a string from a values map
  • hasInterpolation(value) — returns true if the string contains any ${VAR} tokens
  • extractVariables(template) — returns the list of variable names referenced in a template string

New genesis additions

Structured errors

  • EnvError base class extends SuaveplanError with codes under the ENV.* namespace registered via defineCodes
  • Codes:
    • ENV.VALIDATION_FAILED — one or more fields failed validation rules
    • ENV.MISSING_REQUIRED — a required field has no value and no default
    • ENV.INVALID_FORMAT — coercion of a raw string to the target type failed
    • ENV.PARSE_ERROR — dotenv file parse failed or JSON parse failed
  • EnvValidationError, MissingEnvError, CoercionError, DotenvParseError all extend EnvError using the appropriate code
  • All errors carry { key, raw?, source? } context for diagnostics

Zod schemas

  • FieldConfigSchema — validates the FieldConfig<unknown> shape at runtime
  • ValidationResultSchema — validates { ok, errors } produced by validate
  • EnvResultSchema — validates the merged output record from createEnv
  • CreateEnvOptionsSchema — validates options passed to createEnv
  • DotenvLoaderOptionsSchema, ObjectLoaderOptionsSchema — validates loader options at the boundary
  • All schemas exported from *.schemas.ts sibling files

OTel instrumentation on createEnv

  • Counter env.validation.total (labels: outcome: 'success' | 'error') — incremented once per createEnv call
  • Counter env.validation.errors (label: field) — incremented once per field that fails validation
  • Histogram env.load.duration_ms — duration of the full createEnv call including all loaders
  • All meters via @opentelemetry/api; no-op without exporter

Property-based tests (fast-check)

  • Field parsers round-trip: coerce(raw) produces a stable value for all valid raw strings
  • maskSensitive never leaks values: for any schema with sensitive fields, the masked output never contains the original raw value at that key
  • validate accumulates all errors: if N rules fail, ValidationResult.errors.length === N
  • parseDotenv is pure: same input always yields same output

Benchmarks (mitata)

  • createEnv with a 20-field schema, object loader, no errors — hot-path throughput
  • validate hot path — single field with 5 rules, all passing

Impact

  • Adds one new package: packages/universal/env
  • Adds one new spec capability: env
  • No existing package consumers on genesis yet