Initial Commit
This commit is contained in:
201
openspec/archive/2026-04-19-enterprise-env/proposal.md
Normal file
201
openspec/archive/2026-04-19-enterprise-env/proposal.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# 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
|
||||
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.
|
||||
128
openspec/archive/2026-04-19-enterprise-env/tasks.md
Normal file
128
openspec/archive/2026-04-19-enterprise-env/tasks.md
Normal file
@@ -0,0 +1,128 @@
|
||||
> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/config/env.
|
||||
|
||||
# Tasks: enterprise-env
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] 1.1 Create `packages/universal/env/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton
|
||||
- [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul
|
||||
- [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake
|
||||
- [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml`
|
||||
|
||||
## 2. Constants + types + errors
|
||||
|
||||
- [x] 2.1 Implement `src/constants/index.ts` — `DEFAULT_ARRAY_SEPARATOR`, `EMAIL_PATTERN`, `FALSY_VALUES`, `INTERPOLATION_PATTERN`, `PORT_MAX`, `PORT_MIN`, `TRUTHY_VALUES`
|
||||
- [x] 2.2 Implement `src/types/common.ts` — `AnyField`, `CreateEnvOptions`, `EnvError`, `EnvSchema`, `ErrorMode`, `InferEnv`, `InferFieldType`, `Loader`, `LoaderResult`, `OptionalKeys`, `RequiredKeys`, `Simplify`, `ValidationRule`
|
||||
- [x] 2.3 Implement `src/errors/env-error.ts` — `EnvError` base extends `SuaveplanError`; `EnvValidationError`, `MissingEnvError`, `CoercionError`, `DotenvParseError` extend `EnvError`; `defineCodes` for `ENV.VALIDATION_FAILED`, `ENV.MISSING_REQUIRED`, `ENV.INVALID_FORMAT`, `ENV.PARSE_ERROR`
|
||||
- [x] 2.4 Implement `src/errors/error-codes.ts` and auto-register via side-effect import in `src/index.ts`
|
||||
- [x] 2.5 Zod schemas: `FieldConfigSchema`, `ValidationResultSchema`, `CreateEnvOptionsSchema`, `DotenvLoaderOptionsSchema`, `ObjectLoaderOptionsSchema` — exported from `*.schemas.ts` siblings
|
||||
- [x] 2.6 Tests: error construction, code narrowing, `{ key, raw?, source? }` context fields on each error subclass
|
||||
|
||||
## 3. Core validator (validate + maskSensitive)
|
||||
|
||||
- [x] 3.1 Implement `src/core/validator/validator.types.ts` — `ValidateInput`, `ValidationResult`
|
||||
- [x] 3.2 Implement `src/core/validator/validator.ts` — `validate(input): ValidationResult` (runs all `field._rules`, accumulates failures); `maskSensitive(values, schema): Record<string, string>` (replaces sensitive values with `***`)
|
||||
- [x] 3.3 Unit tests: all rules passing, partial failure accumulation, all rules failing, sensitive masking
|
||||
- [x] 3.4 Property-based tests (fast-check): if N rules fail, `result.errors.length === N`; `maskSensitive` never leaks a sensitive raw value
|
||||
- [x] 3.5 Co-located `src/core/validator/validator.md`
|
||||
|
||||
## 4. BaseField abstract class
|
||||
|
||||
- [x] 4.1 Implement `src/core/field/field.types.ts` — `FieldConfig<T>`, `RuleOptions`
|
||||
- [x] 4.2 Implement `src/core/field/field.ts` — `BaseField<T>` abstract class with `_isRequired`, `_hasDefault`, `_defaultValue`, `_isSensitive`, `_description`, `_rules`; fluent builder methods: `optional()`, `default(v)`, `sensitive()`, `describe(s)`, `rules(opts)`; abstract `coerce(raw: string): T`
|
||||
- [x] 4.3 Unit tests: builder method chaining, `FieldConfig` snapshot, abstract coerce enforcement
|
||||
- [x] 4.4 Co-located `src/core/field/field.md`
|
||||
|
||||
## 5. Fields
|
||||
|
||||
- [x] 5.1 `src/fields/string-field/string-field.ts` — `StringField` extends `BaseField<string>`; rules: `minLength`, `maxLength`, `pattern`, `nonempty`; factory `str()`
|
||||
- [x] 5.2 `src/fields/number-field/number-field.ts` — `NumberField` extends `BaseField<number>`; coerces via `coerceNumber`; rules: `min`, `max`, `integer`; factory `num()`
|
||||
- [x] 5.3 `src/fields/boolean-field/boolean-field.ts` — `BooleanField` extends `BaseField<boolean>`; coerces via `coerceBoolean`; factory `bool()`
|
||||
- [x] 5.4 `src/fields/enum-field/enum-field.types.ts` — `EnumValues`; `src/fields/enum-field/enum-field.ts` — `EnumField<V extends EnumValues>` extends `BaseField<V[number]>`; factory `enums(values)`
|
||||
- [x] 5.5 `src/fields/url-field/url-field.ts` — `UrlField` extends `BaseField<string>`; validates via `new URL()`; optional `protocols` allowlist rule; factory `url()`
|
||||
- [x] 5.6 `src/fields/email-field/email-field.ts` — `EmailField` extends `BaseField<string>`; validates against `EMAIL_PATTERN`; factory `email()`
|
||||
- [x] 5.7 `src/fields/port-field/port-field.ts` — `PortField` extends `BaseField<number>`; validates integer in `[PORT_MIN, PORT_MAX]`; factory `port()`
|
||||
- [x] 5.8 `src/fields/json-field/json-field.types.ts` — `JsonValidator<T>`; `src/fields/json-field/json-field.ts` — `JsonField<T>` extends `BaseField<T>`; coerces via `coerceJson`; accepts optional `JsonValidator<T>`; factory `json<T>(validator?)`
|
||||
- [x] 5.9 `src/fields/array-field/array-field.types.ts` — `ItemField<T>`; `src/fields/array-field/array-field.ts` — `ArrayField<T>` extends `BaseField<T[]>`; splits on `separator` (default `DEFAULT_ARRAY_SEPARATOR`), maps each item through `itemField`; factory `array(itemField, separator?)`
|
||||
- [x] 5.10 Unit tests for each field: valid raw, invalid raw → `CoercionError`, all custom rules, default / optional / sensitive combinations
|
||||
- [x] 5.11 Co-located `.md` for each field file
|
||||
|
||||
## 6. Transforms (coerce utilities)
|
||||
|
||||
- [x] 6.1 Implement `src/transforms/coerce/coerce.types.ts` — `CoerceFn<T>`, `CoerceEntry<T>`
|
||||
- [x] 6.2 Implement `src/transforms/coerce/coerce.ts` — `coerceString`, `coerceNumber`, `coerceInteger`, `coerceBoolean`, `coerceArray`, `coerceJson`; each throws `CoercionError` with `ENV.INVALID_FORMAT` on failure
|
||||
- [x] 6.3 Unit tests: happy-path and all rejection branches for each coerce function
|
||||
- [x] 6.4 Property-based tests (fast-check): `coerceNumber(raw)` round-trips for all finite float strings; `coerceBoolean` is total over TRUTHY_VALUES ∪ FALSY_VALUES
|
||||
- [x] 6.5 Co-located `src/transforms/coerce/coerce.md`
|
||||
|
||||
## 7. Loaders
|
||||
|
||||
- [x] 7.1 `src/loaders/object/object.ts` — `objectLoader(obj, options?)` → `Loader`; `ObjectLoaderOptions`; source label defaults to `'object'`
|
||||
- [x] 7.2 `src/loaders/dotenv/dotenv.ts` — `parseDotenv(content): Record<string, string>` (pure, throws `DotenvParseError` on malformed lines); `dotenvLoader(options?)` → `Loader` reads file from `options.path` (default `.env`), calls `parseDotenv`; `DotenvLoaderOptions`
|
||||
- [x] 7.3 Unit tests for `objectLoader`: wraps object, respects source label
|
||||
- [x] 7.4 Unit tests for `parseDotenv`: quoted values, comments, blank lines, `=` in value, malformed input
|
||||
- [x] 7.5 Unit tests for `dotenvLoader`: reads real temp file, file-not-found returns empty gracefully (or throws — match main-branch behaviour), `override` flag
|
||||
- [x] 7.6 Property-based tests (fast-check): `parseDotenv` is pure — same input always produces same output
|
||||
- [x] 7.7 Co-located `.md` for each loader file
|
||||
|
||||
## 8. Utilities — interpolation + error factory
|
||||
|
||||
- [x] 8.1 `src/utils/interpolation.ts` — `interpolate(template, values)`, `hasInterpolation(value)`, `extractVariables(template)`; uses `INTERPOLATION_PATTERN`
|
||||
- [x] 8.2 `src/utils/errors.ts` — `createEnvError(key, message, value?)` factory returning `EnvError` value object (not the class — the plain interface shape used by `CollectedErrors`)
|
||||
- [x] 8.3 Unit tests for all interpolation functions and the error factory
|
||||
- [x] 8.4 Co-located `.md` for each utility file
|
||||
|
||||
## 9. createEnv orchestrator
|
||||
|
||||
- [x] 9.1 Implement `src/core/create-env/create-env.types.ts` — `CollectedErrors`, `EnvResult<S>`
|
||||
- [x] 9.2 Implement `src/core/create-env/create-env.ts` — `createEnv(schema, options?)`:
|
||||
- Runs each `Loader` in `options.loaders` in order, merging `LoaderResult.values` (later loaders win unless `override: false`)
|
||||
- Expands `${VAR}` interpolation tokens in raw values
|
||||
- For each schema key: resolves raw string → calls `coerce` → runs `validate` → checks `isRequired`
|
||||
- In `'throw'` mode: throws `EnvValidationError` or `MissingEnvError` on first failure
|
||||
- In `'collect'` mode: accumulates all failures into `CollectedErrors`, returns partial result
|
||||
- In `'warn'` mode: logs via `console.warn`, returns partial result
|
||||
- Calls `maskSensitive` before returning
|
||||
- Returns `EnvResult<S>` (typed record)
|
||||
- [x] 9.3 Unit tests: full schema resolution, missing required, coercion error, collect mode accumulation, throw mode, warn mode, multi-loader merge order, interpolation expansion
|
||||
- [x] 9.4 Co-located `src/core/create-env/create-env.md`
|
||||
|
||||
## 10. Presets
|
||||
|
||||
- [x] 10.1 `src/presets/common/common.ts` — `nodeEnvField`, `portField`, `hostField`, `logLevelField`, `debugField`, `appNameField`, `appVersionField`, `timezoneField`, `workersField`, `commonFields`
|
||||
- [x] 10.2 `src/presets/server/server.ts` — `databaseUrlField`, `redisUrlField`, `sessionSecretField`, `corsOriginField`, `rateLimitMaxField`, `rateLimitWindowField`, `trustProxyField`, `smtpHostField`, `smtpPortField`, `smtpUserField`, `smtpPassField`, `smtpFromField`, `serverFields`
|
||||
- [x] 10.3 Unit tests: each preset field resolves correctly from a matching env object; `commonFields` and `serverFields` composites contain all expected keys
|
||||
- [x] 10.4 Co-located `.md` for each preset file
|
||||
|
||||
## 11. OTel instrumentation
|
||||
|
||||
- [x] 11.1 Add `@opentelemetry/api` to `dependencies` in `package.json`
|
||||
- [x] 11.2 Wire counter `env.validation.total` (label `outcome: 'success' | 'error'`) in `createEnv` — incremented once per call
|
||||
- [x] 11.3 Wire counter `env.validation.errors` (label `field`) in `createEnv` — incremented once per field that fails validation
|
||||
- [x] 11.4 Wire histogram `env.load.duration_ms` — duration of the full `createEnv` call
|
||||
- [x] 11.5 All meters obtained from `@opentelemetry/api` `getMeter`; no-op without exporter (verified in test with no SDK registered)
|
||||
- [x] 11.6 Unit test: metrics instruments are created without error when no exporter is present
|
||||
|
||||
## 12. Property tests + benchmarks
|
||||
|
||||
- [x] 12.1 Property-based tests (fast-check) for field parsers round-trip (valid raw → coerce → stable value)
|
||||
- [x] 12.2 Property test: `maskSensitive` never leaks raw values for sensitive fields
|
||||
- [x] 12.3 Property test: `validate` accumulates all N failing rules
|
||||
- [x] 12.4 Property test: `parseDotenv` is pure and idempotent
|
||||
- [x] 12.5 `src/__benchmarks__/create-env.bench.ts` — `createEnv` with 20-field schema, `objectLoader`, no errors; throughput via mitata
|
||||
- [x] 12.6 `src/__benchmarks__/validate.bench.ts` — `validate` hot path: single field with 5 passing rules; iterations via mitata
|
||||
- [x] 12.7 Publish baseline numbers in README
|
||||
|
||||
## 13. Docs
|
||||
|
||||
- [x] 13.1 Co-located `.md` for every implementation file (≥200 words, simple + advanced examples, API reference, implementation notes)
|
||||
- [x] 13.2 Rewrite `README.md` with public API table, loader usage, preset reference, error code table, OTel metric names, benchmark numbers, consumer quick-start
|
||||
- [x] 13.3 `bun run docs:check --filter=@suaveplan/env` ≥ 90%
|
||||
|
||||
## 14. Verify and publish
|
||||
|
||||
- [x] 14.1 `bunx turbo lint typecheck test --filter=@suaveplan/env` green
|
||||
- [x] 14.2 `bunx turbo test:coverage --filter=@suaveplan/env` at 100/100/100/100
|
||||
- [x] 14.3 `bunx turbo build --filter=@suaveplan/env` green
|
||||
- [x] 14.4 `bun scripts/publish.ts --package @suaveplan/env` to Verdaccio at 0.0.0
|
||||
- [x] 14.5 Archive OpenSpec change to `openspec/archive/2026-04-19-enterprise-env/`
|
||||
Reference in New Issue
Block a user