Environment Manager
EnvironmentConfigManager is the engine that assembles configuration values from all layers and returns a single resolved result. You give it one or more ConfigSchema objects and a description of the runtime environment; it performs the load, validates required fields, evaluates policies, and records provenance for every key.
Key Concepts
- Load result — a single call to
.load()producesConfigLoadResult, containing resolved values, layer snapshots, provenance, missing required keys, and policy violations. - Layer snapshot — an immutable record of what changed in one layer (e.g.,
environment,secrets,overrides). - Provenance map — a per-key history of every layer that touched a value, including the timestamp and the redacted display value.
- Policy violations — fields whose values fail a
ConfigPolicy(min/max/pattern/enum) are accumulated without throwing, letting you decide how to handle them.
EnvironmentConfigManager Class
Constructor Options
interface EnvironmentConfigOptions {
schemas: ConfigSchema[]; // One or more schemas built with buildConfigSchema()
environment: string; // Active environment name, e.g. 'production'
workspaceRoot: string; // Absolute path to the project root
configDir?: string; // Directory to search for manifest files; default {workspaceRoot}/config
manifestFile?: string; // Explicit path to a YAML/JSON manifest (skips discovery)
overrides?: Record<string, unknown>; // Namespace-keyed programmatic overrides (highest precedence)
secretProvider?: SecretProvider; // Custom secret provider; default CompositeSecretProvider
metricEmitterFactory?: (ctx: ConfigMetricContext) => ConfigMetricEmitter;
}
When secretProvider is omitted, a CompositeSecretProvider is created automatically. It first tries environment variables with the SECRET_ prefix, then falls back to {configDir}/secrets.json.
.load() Method
async load(): Promise<ConfigLoadResult>
Performs a full layered load and returns:
interface ConfigLoadResult {
values: Record<string, any>; // Namespace → resolved field values
layers: ConfigLayerSnapshot[]; // Ordered list of layers that contributed values
provenance: ConfigProvenanceMap; // key → list of provenance entries
missingRequired: string[]; // Fully-qualified keys that are required but absent
policyViolations: ConfigPolicyViolation[]; // Keys whose values violate policy constraints
}
Layer Snapshots
Each layer that contributes at least one value appears in ConfigLoadResult.layers.
interface ConfigLayerSnapshot {
name: string; // 'schema-defaults' | 'environment' | 'manifest' | 'secrets' | 'overrides'
details?: string; // Optional human-readable description
filePath?: string; // Set for the 'manifest' layer
changes: ConfigLayerChange[];
}
interface ConfigLayerChange {
key: string; // Fully-qualified key, e.g. 'database.host'
value: unknown; // Redacted for sensitive fields
source?: string; // Source identifier, e.g. 'env:DATABASE_HOST' or '/config/production.yaml'
reason?: string;
sensitive?: boolean;
}
Provenance Map
type ConfigProvenanceMap = Record<string, ConfigProvenanceEntry[]>;
interface ConfigProvenanceEntry {
key: string;
value: unknown; // Redacted for sensitive fields
source?: string;
layer: string;
timestamp: number; // Unix milliseconds when the entry was recorded
sensitive?: boolean;
}
The entries for each key are ordered from earliest (lowest precedence) to latest (highest precedence). The last entry always reflects the winning value.
Policy Violations
interface ConfigPolicyViolation {
key: string; // Fully-qualified key
reason: string; // 'below-minimum' | 'above-maximum' | 'pattern-mismatch' | 'enum-mismatch'
expected?: string; // The policy constraint that was not met
actual?: unknown; // The actual value that violated the constraint
}
Violations are accumulated in ConfigLoadResult.policyViolations and do not cause .load() to throw. Your application decides whether to treat them as fatal.
Manifest File Discovery
When manifestFile is not specified, the manager looks for the following files under configDir in order:
{environment}.yaml{environment}.yml{environment}.json
The first file found is parsed (YAML or JSON) and applied as the manifest layer. The manifest object must have namespace names as its top-level keys.
# config/production.yaml
database:
host: db.example.com
port: 5432
cache:
ttl: 3600
Basic Usage Example
import 'reflect-metadata';
import {
AIConfig,
AIEnv,
buildConfigSchema,
EnvironmentConfigManager,
} from '@tagna/udiot';
@AIConfig({ namespace: 'database' })
class DatabaseConfig {
@AIEnv({ required: true })
host!: string;
@AIEnv({ type: 'number', default: 5432 })
port!: number;
@AIEnv({ sensitive: true })
password!: string;
}
@AIConfig({ namespace: 'app' })
class AppConfig {
@AIEnv({ default: 'MyApp' })
name!: string;
}
const manager = new EnvironmentConfigManager({
schemas: [buildConfigSchema(DatabaseConfig), buildConfigSchema(AppConfig)],
environment: process.env.NODE_ENV ?? 'development',
workspaceRoot: process.cwd(),
});
const result = await manager.load();
if (result.missingRequired.length > 0) {
console.error('Missing required configuration:', result.missingRequired);
process.exit(1);
}
if (result.policyViolations.length > 0) {
console.warn('Policy violations:', result.policyViolations);
}
const dbHost = result.values['database'].host;
console.log('Database host:', dbHost);
Printing Provenance for a Key
const entries = result.provenance['database.host'] ?? [];
for (const entry of entries) {
console.log(`[${entry.layer}] ${entry.key} = ${entry.value} (from ${entry.source})`);
}
// Example output:
// [schema-defaults] database.host = undefined (from schema-default)
// [environment] database.host = db.example.com (from env:DATABASE_HOST)
Common Patterns
Using runtime overrides for testing
const manager = new EnvironmentConfigManager({
schemas: [buildConfigSchema(DatabaseConfig)],
environment: 'test',
workspaceRoot: process.cwd(),
overrides: {
database: { host: 'localhost', port: 5433 },
},
});
Overrides sit at the highest precedence layer so they reliably win regardless of what is in the environment or manifest files.
Treating missing required keys as a fatal startup error
const result = await manager.load();
const problems = [
...result.missingRequired.map(k => `Missing required: ${k}`),
...result.policyViolations.map(v => `Policy violation on ${v.key}: ${v.reason} (expected ${v.expected}, got ${v.actual})`),
];
if (problems.length > 0) {
throw new Error(`Configuration errors:\n${problems.join('\n')}`);
}
Integration Notes
EnvironmentConfigManagerdoes not modifyprocess.env. All loading is read-only.- Sensitive values are redacted (replaced with
'[REDACTED]') inConfigLayerChange.valueandConfigProvenanceEntry.value. The actual value is only present inConfigLoadResult.values. - The manager emits metrics through
ConfigMetricEmitterfor every field usage and validation failure, which are visible in your OpenTelemetry metrics backend.
Pitfalls
- Calling
.load()multiple times creates new provenance entries on each call. If you need the result more than once, cache it. - The
manifestlayer is loaded asynchronously from disk. If the file is modified between application starts, the result changes. Do not assume manifests are immutable across deployments without version-pinning them. - Policy violations on
sensitivefields will showactual: '[REDACTED]'rather than the real value.