202 lines
10 KiB
Markdown
202 lines
10 KiB
Markdown
# 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
|