Docs/configurationv1.0.0-rc.1

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() produces ConfigLoadResult, 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:

  1. {environment}.yaml
  2. {environment}.yml
  3. {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

  • EnvironmentConfigManager does not modify process.env. All loading is read-only.
  • Sensitive values are redacted (replaced with '[REDACTED]') in ConfigLayerChange.value and ConfigProvenanceEntry.value. The actual value is only present in ConfigLoadResult.values.
  • The manager emits metrics through ConfigMetricEmitter for 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 manifest layer 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 sensitive fields will show actual: '[REDACTED]' rather than the real value.
From the canonical UDIOT public documentation.Try it in the playground ↗