Initial Commit
This commit is contained in:
127
openspec/archive/2026-04-19-enterprise-env/specs/env/spec.md
vendored
Normal file
127
openspec/archive/2026-04-19-enterprise-env/specs/env/spec.md
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user