# 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` — the resolved typed record produced by `createEnv` - `BaseField` — abstract base class for all field builders; holds rules, default, sensitive flag, description - `FieldConfig` — 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` — 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` used as the schema argument to `createEnv` - `ErrorMode` — `'throw' | 'collect' | 'warn'` - `InferEnv` — maps an `EnvSchema` to the resulting typed record type - `InferFieldType` — extracts the value type from a field builder - `Loader` — `{ name: string; load(): LoaderResult }` interface for env sources - `LoaderResult` — `{ values: Record; source: string }` - `OptionalKeys` — keys whose fields may produce `undefined` - `RequiredKeys` — keys whose fields always produce a value - `Simplify` — intersection flattener utility type - `ValidationRule` — `{ 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` / `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` / `json` — JSON field; accepts a `JsonValidator` for runtime shape checking - `JsonValidator` — `(parsed: unknown) => T` used with `JsonField` - `ArrayField` / `array` — array field; splits by `DEFAULT_ARRAY_SEPARATOR` (or custom), maps each item through an `ItemField` - `ItemField` — 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` - `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` — `(raw: string) => T` - `CoerceEntry` — `{ coerce: CoerceFn }` 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` 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