10 KiB
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:observabilitytslib→catalog:build
Dev deps (via catalog):
vitest,@vitest/coverage-istanbul→catalog:testingfast-check→catalog:testingmitata→catalog:testing
Preserved public API surface (from main branch)
Constants
DEFAULT_ARRAY_SEPARATOR— default delimiter used byArrayFieldEMAIL_PATTERN— regex for RFC-5322-compatible email validationFALSY_VALUES— string values coerced tofalsebyBooleanFieldINTERPOLATION_PATTERN— regex matching${VAR}tokens in raw valuesPORT_MAX— upper bound for valid TCP port (65535)PORT_MIN— lower bound for valid TCP port (1)TRUTHY_VALUES— string values coerced totruebyBooleanField
Core
createEnv(schema, options?)— loads env vars through configured loaders, validates all fields, collects or throws errors, returns typed recordCollectedErrors— map of field key → array of error messages produced during a collect-mode runEnvResult<S>— the resolved typed record produced bycreateEnvBaseField<T>— abstract base class for all field builders; holds rules, default, sensitive flag, descriptionFieldConfig<T>— runtime configuration snapshot extracted from anyBaseFieldRuleOptions— options accepted byBaseField.rules()for adding a named validation rulemaskSensitive(values, schema)— replaces sensitive field values with***in the result mapvalidate(input)→ValidationResult— runs all rules for a single field, accumulating failuresValidateInput—{ key, raw, field, coerced? }fed intovalidateValidationResult—{ ok, errors }whereerrorsis an array of{ rule, message }pairs
Types
AnyField<T, R>— minimal interface every field builder satisfies; used for schema mapsCreateEnvOptions— options accepted bycreateEnv:loaders,errorMode,signal?EnvError—{ key, message, value? }shape for a single validation failureEnvSchema—Record<string, AnyField>used as the schema argument tocreateEnvErrorMode—'throw' | 'collect' | 'warn'InferEnv<S>— maps anEnvSchemato the resulting typed record typeInferFieldType<F>— extracts the value type from a field builderLoader—{ name: string; load(): LoaderResult }interface for env sourcesLoaderResult—{ values: Record<string, string>; source: string }OptionalKeys<S>— keys whose fields may produceundefinedRequiredKeys<S>— keys whose fields always produce a valueSimplify<T>— intersection flattener utility typeValidationRule<T>—{ name, message, validate(value): boolean }
Fields
StringField/str— string field builder; rules:minLength,maxLength,pattern,nonemptyNumberField/num— number field; rules:min,max,integerBooleanField/bool— boolean field; coerces TRUTHY_VALUES / FALSY_VALUES stringsEnumField<V>/enums— enum field locked to a declaredEnumValuestupleEnumValues— string-tuple type constraint forEnumFieldUrlField/url— URL field; validates viaURLconstructor, optional protocol restrictionEmailField/email— email field; validates againstEMAIL_PATTERNPortField/port— port field; integer in[PORT_MIN, PORT_MAX]JsonField<T>/json— JSON field; accepts aJsonValidator<T>for runtime shape checkingJsonValidator<T>—(parsed: unknown) => Tused withJsonFieldArrayField<T>/array— array field; splits byDEFAULT_ARRAY_SEPARATOR(or custom), maps each item through anItemFieldItemField<T>— per-element field descriptor forArrayField
Loaders
dotenvLoader(options?)— reads a.envfile from disk and returns aLoaderResultparseDotenv(content)— pure function: parses a dotenv-format string into aRecord<string, string>DotenvLoaderOptions—{ path?, encoding?, override? }objectLoader(obj, options?)— wraps an in-memory object as a loaderObjectLoaderOptions—{ source? }(source label for diagnostics)
Presets — common
nodeEnvField—NODE_ENVenum field locked to'development' | 'test' | 'production' | 'staging'portField—PORTwith default 3000hostField—HOSTwith default'0.0.0.0'logLevelField—LOG_LEVELenum locked to standard logger levelsdebugField—DEBUGboolean with defaultfalseappNameField—APP_NAMErequired stringappVersionField—APP_VERSIONstring with default'0.0.0'timezoneField—TZstring with default'UTC'workersField—WORKERSinteger numbercommonFields— composite preset:nodeEnvField + portField + hostField + logLevelField + debugField
Presets — server
databaseUrlField—DATABASE_URLsensitive required URLredisUrlField—REDIS_URLsensitive URLsessionSecretField—SESSION_SECRETsensitive required string withminLength: 32corsOriginField—CORS_ORIGINstring or arrayrateLimitMaxField—RATE_LIMIT_MAXinteger numberrateLimitWindowField—RATE_LIMIT_WINDOW_MSinteger numbertrustProxyField—TRUST_PROXYboolean with defaultfalsesmtpHostField—SMTP_HOSTstringsmtpPortField—SMTP_PORTport fieldsmtpUserField—SMTP_USERstringsmtpPassField—SMTP_PASSsensitive stringsmtpFromField—SMTP_FROMemail fieldserverFields— composite: common server connection + SMTP fields
Transforms (coerce utilities)
coerceString(raw)— identity / trimcoerceNumber(raw)— parses float, rejects NaNcoerceInteger(raw)— parses integer, rejects non-integercoerceBoolean(raw)— maps truthy/falsy string setscoerceArray(raw, separator?)— splits and trimscoerceJson(raw)—JSON.parsewith structured error on failureCoerceFn<T>—(raw: string) => TCoerceEntry<T>—{ coerce: CoerceFn<T> }mixin used internally
Utilities
EnvValidationErrorextendsSuaveplanError— thrown in'throw'error modeMissingEnvErrorextendsSuaveplanError— thrown when a required key is absentCoercionErrorextendsSuaveplanError— thrown when coercion fails for a raw valueDotenvParseErrorextendsSuaveplanError— thrown byparseDotenvon malformed inputcreateEnvError(key, message, value?)— factory forEnvErrorvalue objectsinterpolate(template, values)— resolves${VAR}tokens in a string from a values maphasInterpolation(value)— returnstrueif the string contains any${VAR}tokensextractVariables(template)— returns the list of variable names referenced in a template string
New genesis additions
Structured errors
EnvErrorbase class extendsSuaveplanErrorwith codes under theENV.*namespace registered viadefineCodes- Codes:
ENV.VALIDATION_FAILED— one or more fields failed validation rulesENV.MISSING_REQUIRED— a required field has no value and no defaultENV.INVALID_FORMAT— coercion of a raw string to the target type failedENV.PARSE_ERROR— dotenv file parse failed or JSON parse failed
EnvValidationError,MissingEnvError,CoercionError,DotenvParseErrorall extendEnvErrorusing the appropriate code- All errors carry
{ key, raw?, source? }context for diagnostics
Zod schemas
FieldConfigSchema— validates theFieldConfig<unknown>shape at runtimeValidationResultSchema— validates{ ok, errors }produced byvalidateEnvResultSchema— validates the merged output record fromcreateEnvCreateEnvOptionsSchema— validates options passed tocreateEnvDotenvLoaderOptionsSchema,ObjectLoaderOptionsSchema— validates loader options at the boundary- All schemas exported from
*.schemas.tssibling files
OTel instrumentation on createEnv
- Counter
env.validation.total(labels:outcome: 'success' | 'error') — incremented once percreateEnvcall - Counter
env.validation.errors(label:field) — incremented once per field that fails validation - Histogram
env.load.duration_ms— duration of the fullcreateEnvcall 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 validrawstrings maskSensitivenever leaks values: for any schema with sensitive fields, the masked output never contains the original raw value at that keyvalidateaccumulates all errors: if N rules fail,ValidationResult.errors.length === NparseDotenvis pure: same input always yields same output
Benchmarks (mitata)
createEnvwith a 20-field schema, object loader, no errors — hot-path throughputvalidatehot 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