181 lines
4.9 KiB
Markdown
181 lines
4.9 KiB
Markdown
# config
|
|
|
|
**Category**: universal
|
|
**Path**: `universal/config/config`
|
|
**Archived from**: `openspec/archive/2026-04-24-enterprise-config/`
|
|
|
|
---
|
|
|
|
## Purpose
|
|
|
|
Layered configuration management for the Suaveplan monorepo. Five typed layers (defaults, file, object, env, CLI) are resolved in priority order, deep-merged, optionally validated via a caller-supplied Zod-compatible schema, and exposed through an immutable `ConfigStore` with hot-reload and subscription support.
|
|
|
|
## Requirements
|
|
|
|
## Constants
|
|
|
|
```ts
|
|
LAYER_PRIORITIES: { DEFAULTS: 0; FILE: 10; OBJECT: 20; ENV: 30; CLI: 40 }
|
|
MASK_STRING: "****"
|
|
TRUTHY_STRINGS: string[]
|
|
FALSY_STRINGS: string[]
|
|
```
|
|
|
|
---
|
|
|
|
## Error Codes
|
|
|
|
| Code | Severity | Retryable | Trigger |
|
|
|---|---|---|---|
|
|
| `CONFIG_MISSING` | `error` | `false` | Required config key not present |
|
|
| `CONFIG_INVALID_TYPE` | `error` | `false` | Value has wrong type |
|
|
| `CONFIG_PARSE_FAILED` | `error` | `false` | Layer resolution threw |
|
|
| `CONFIG_FILE_NOT_FOUND` | `error` | `false` | File layer content unavailable |
|
|
| `CONFIG_ENV_NOT_SET` | `error` | `false` | Required env var absent |
|
|
| `CONFIG_RELOAD_FAILED` | `error` | `true` | Reload aborted or layer re-resolution threw |
|
|
| `CONFIG_VALIDATION_FAILED` | `fatal` | `false` | Validator threw |
|
|
|
|
---
|
|
|
|
## Core API
|
|
|
|
```ts
|
|
class ConfigError extends SuaveplanError<ConfigPackageErrorCode>
|
|
|
|
function createConfig<T extends ConfigData>(options: CreateConfigOptions<T>): ConfigStore<T>
|
|
|
|
interface ConfigErrorDetail { path?: string; message: string }
|
|
interface MergeResult { data: ConfigData; layers: ResolvedLayer[] }
|
|
interface ResolvedLayer { name: string; priority: number; data: ConfigData }
|
|
```
|
|
|
|
---
|
|
|
|
## Store
|
|
|
|
```ts
|
|
function createConfigStore<T extends ConfigData>(options: ConfigStoreOptions<T>): ConfigStore<T>
|
|
|
|
interface ConfigStoreOptions<T extends ConfigData = ConfigData> {
|
|
data: T
|
|
sensitive: string[]
|
|
}
|
|
|
|
interface ConfigStore<T extends ConfigData = ConfigData> {
|
|
get<K extends string>(key: K): unknown
|
|
has(key: string): boolean
|
|
toJSON(): Readonly<T>
|
|
toMasked(): Readonly<T>
|
|
reload(signal?: AbortSignal): Promise<void>
|
|
subscribe(listener: (next: Readonly<T>) => void): () => void
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Layers
|
|
|
|
```ts
|
|
function defaultsLayer<T extends ConfigData>(defaults: T): ConfigLayer
|
|
function objectLayer(data: ConfigData, priority?: number): ConfigLayer
|
|
function envLayer(options: EnvLayerOptions): ConfigLayer
|
|
function fileLayer(options: FileLayerOptions): ConfigLayer
|
|
function cliLayer(options: CliLayerOptions): ConfigLayer
|
|
function parseArgs(args: string[], mapping: Record<string, string>): Record<string, string>
|
|
```
|
|
|
|
---
|
|
|
|
## Types
|
|
|
|
```ts
|
|
type ConfigData = Record<string, unknown>
|
|
|
|
interface ConfigLayer {
|
|
readonly name: string
|
|
readonly priority: number
|
|
resolve(): ConfigData
|
|
teardown?(): void
|
|
}
|
|
|
|
type ConfigValidator<T extends ConfigData = ConfigData> = (data: ConfigData) => T
|
|
|
|
type EnvironmentOverrides<T extends ConfigData = ConfigData> = Record<string, Partial<T>>
|
|
|
|
interface CreateConfigOptions<T extends ConfigData = ConfigData> {
|
|
layers: ConfigLayer[]
|
|
validate?: ConfigValidator<T>
|
|
sensitive?: string[]
|
|
environments?: EnvironmentOverrides<T>
|
|
environment?: string
|
|
logger?: LoggerLike
|
|
}
|
|
|
|
interface EnvLayerOptions {
|
|
prefix?: string
|
|
mapping: Record<string, string>
|
|
source?: Record<string, string | undefined>
|
|
}
|
|
|
|
interface CliLayerOptions {
|
|
args: string[]
|
|
mapping: Record<string, string>
|
|
}
|
|
|
|
interface FileLayerOptions {
|
|
content: ConfigData
|
|
watchBus?: EventBusLike
|
|
watchEvent?: string
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Utils
|
|
|
|
```ts
|
|
function deepMerge<T extends ConfigData>(base: T, override: Partial<T>): T
|
|
function mergeLayers(layers: Array<{ name: string; data: ConfigData }>): ConfigData
|
|
function getByPath(obj: ConfigData, path: string): unknown
|
|
function hasByPath(obj: ConfigData, path: string): boolean
|
|
function setByPath(obj: ConfigData, path: string, value: unknown): ConfigData
|
|
function maskSensitive(data: ConfigData, sensitive: string[], mask?: string): ConfigData
|
|
function coerceValue(raw: string): unknown
|
|
```
|
|
|
|
---
|
|
|
|
## Zod Schemas
|
|
|
|
- `EnvLayerOptionsSchema`
|
|
- `CliLayerOptionsSchema`
|
|
- `FileLayerOptionsSchema`
|
|
- `ConfigStoreOptionsSchema`
|
|
- `CreateConfigOptionsSchema`
|
|
|
|
---
|
|
|
|
## OTel Instrumentation
|
|
|
|
| Signal | Name | Attributes |
|
|
|---|---|---|
|
|
| Span | `config.create` | `config.layer_count`, `config.environment` |
|
|
| Counter | `config.get_total` | `config.hit` (bool) |
|
|
| Counter | `config.reload_total` | — |
|
|
| Histogram | `config.reload_duration_ms` | — |
|
|
| Histogram | `config.merge_duration_ms` | `config.layer_count` |
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
| Package | Role |
|
|
|---|---|
|
|
| `@suaveplan/error` | `SuaveplanError` base class, `defineCodes` |
|
|
| `@suaveplan/types` | `LoggerLike`, `EventBusLike` |
|
|
| `@suaveplan/events` | `EventBus` for file-layer watch mode |
|
|
| `@suaveplan/env` | `coerceValue` boolean/numeric token reference |
|
|
| `@opentelemetry/api` | OTel spans, counters, histograms |
|
|
| `zod` | Zod schemas for all public option types |
|
|
| `tslib` | TS runtime helpers |
|