first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
# @suaveplan/config - Layered configuration management: defaults, file, object, env, CLI layers merged in priority order with deep-merge, Zod validation, OTel instrumentation, and event-bus watch support.
## Summary
Layered configuration management for Suaveplan. Resolves an ordered set of typed layers (defaults, file, object, env, CLI) in priority order, deep-merges the results, applies per-environment overrides, validates via a caller-supplied Zod schema or custom function, and exposes an immutable `ConfigStore` with live reload and event-bus change propagation.
<!-- begin:auto-docs -->
## Documentation tree
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
<!-- end:auto-docs --> - [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_ - [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
## Installation
```bash
bun add @suaveplan/config
```
## Quick Start
```ts
import { createConfig, defaultsLayer, fileLayer, envLayer, cliLayer } from '@suaveplan/config';
import { readFileSync } from 'node:fs';
import z from 'zod';
const Schema = z.object({
server: z.object({ port: z.number(), host: z.string() }),
db: z.object({ url: z.string(), pool: z.number(), password: z.string() }),
log: z.object({ level: z.string() }),
});
type AppConfig = z.infer<typeof Schema>;
const store = createConfig<AppConfig>({
layers: [
defaultsLayer({ server: { port: 3000, host: '0.0.0.0' }, db: { pool: 5, password: '' }, log: { level: 'info' } }),
fileLayer({ content: JSON.parse(readFileSync('./config.json', 'utf-8')) }),
envLayer({ prefix: 'APP_', mapping: { PORT: 'server.port', DB_URL: 'db.url', DB_PASS: 'db.password' } }),
cliLayer({ args: process.argv.slice(2), mapping: { port: 'server.port' } }),
],
validate: (raw) => Schema.parse(raw),
sensitive: ['db.password', 'db.url'],
environments: { production: { log: { level: 'warn' } }, development: { log: { level: 'debug' } } },
environment: process.env.NODE_ENV ?? 'development',
});
console.log(store.get('server.port')); // number
console.log(store.toMasked()); // db.password = '****'
// Live reload
await store.reload();
// Subscribe to changes
const unsub = store.subscribe((next) => console.log('Config updated:', next));
```
## Module Documentation
| Module | Description |
|--------|-------------|
| [`src/types/common.md`](src/types/common.md) | All public TypeScript interfaces and type aliases |
| [`src/types/schemas.md`](src/types/schemas.md) | Zod option schemas for runtime validation |
| [`src/core/config-builder/config-builder.md`](src/core/config-builder/config-builder.md) | `createConfig` orchestrator |
| [`src/core/config-store/config-store.md`](src/core/config-store/config-store.md) | `createConfigStore` static store factory |
| [`src/layers/defaults-layer/defaults-layer.md`](src/layers/defaults-layer/defaults-layer.md) | Static default values layer |
| [`src/layers/object-layer/object-layer.md`](src/layers/object-layer/object-layer.md) | In-memory config object layer |
| [`src/layers/env-layer/env-layer.md`](src/layers/env-layer/env-layer.md) | Environment variable layer + `coerceValue` |
| [`src/layers/file-layer/file-layer.md`](src/layers/file-layer/file-layer.md) | Pre-parsed file content layer with watch mode |
| [`src/layers/cli-layer/cli-layer.md`](src/layers/cli-layer/cli-layer.md) | CLI argument layer + `parseArgs` |
| [`src/utils/merge.md`](src/utils/merge.md) | `deepMerge` and `mergeLayers` utilities |
| [`src/utils/mask.md`](src/utils/mask.md) | Path utilities and `maskSensitive` |
## Layer Priorities
| Layer | Priority | Description |
|-------|----------|-------------|
| `defaultsLayer` | 0 | Static baseline — always overridden |
| `fileLayer` | 10 | Pre-parsed file content |
| `objectLayer` | 20 | In-memory programmatic overrides |
| `envLayer` | 30 | Environment variables |
| `cliLayer` | 40 | CLI arguments — highest priority |
## API
### Core
| Export | Description |
|--------|-------------|
| `createConfig(options)` | Resolves layers, merges, validates, returns live store |
| `createConfigStore(options)` | Low-level static store factory |
### Layers
| Export | Description |
|--------|-------------|
| `defaultsLayer(defaults)` | Static defaults at priority 0 |
| `fileLayer(options)` | Pre-parsed file content at priority 10 |
| `objectLayer(data, priority?)` | In-memory object at priority 20 |
| `envLayer(options)` | Environment variables at priority 30 |
| `cliLayer(options)` | CLI arguments at priority 40 |
| `parseArgs(args, mapping)` | Parse raw argv into flag→value map |
### Utilities
| Export | Description |
|--------|-------------|
| `deepMerge(base, override)` | Recursive object merge |
| `mergeLayers(layers)` | Fold ordered layers with deepMerge |
| `getByPath(obj, path)` | Read dot-notation path |
| `setByPath(obj, path, value)` | Write dot-notation path |
| `hasByPath(obj, path)` | Check dot-notation path existence |
| `maskSensitive(data, paths, mask?)` | Replace sensitive paths |
| `coerceValue(raw)` | Convert env-var string to typed value |
### Error Codes
| Code | Description |
|------|-------------|
| `CONFIG.MISSING` | Required config key not found |
| `CONFIG.INVALID_TYPE` | Value has wrong type |
| `CONFIG.PARSE_FAILED` | Layer resolution threw |
| `CONFIG.FILE_NOT_FOUND` | File layer file not found |
| `CONFIG.ENV_NOT_SET` | Required env var not set |
| `CONFIG.RELOAD_FAILED` | Reload aborted or layer threw |
| `CONFIG.VALIDATION_FAILED` | Validator threw |
## Dependencies
| Package | Role |
|---------|------|
| `@suaveplan/error` | `SuaveplanError` base, `defineCodes` |
| `@suaveplan/types` | `LoggerLike`, `EventBusLike` |
| `@suaveplan/events` | `EventBus` for file-layer watch mode |
| `@suaveplan/env` | Coercion functions for env-var values |
| `zod` | Option schemas |
| `@opentelemetry/api` | Spans, counters, histograms |
| `tslib` | TypeScript runtime helpers |
## Benchmarks
Run with `bun run bench`.
| Benchmark | Operations / sec (approx.) |
|-----------|---------------------------|
| `createConfig` 5 layers (cold) | TBD |
| `store.get` 100x deeply nested key | TBD |
| `mergeLayers` 2 layers | TBD |
| `mergeLayers` 5 layers | TBD |
| `mergeLayers` 10 layers | TBD |
| `mergeLayers` 20 layers | TBD |
_Run `bun run bench` to populate these numbers on your hardware._

View File

@@ -0,0 +1,108 @@
# config-builder — `createConfig` orchestrator for `@suaveplan/config`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createConfig` is the primary entry point to `@suaveplan/config`. It accepts
a list of `ConfigLayer` objects, resolves each in turn, sorts them by priority,
deep-merges the results, applies any per-environment overrides, runs the
caller-supplied validator, and returns a live `ConfigStore`. The live store
supports `reload()` to re-run the full pipeline and `subscribe()` to receive
change notifications after each successful reload. All lifecycle events are
traced via OTel spans and optionally logged via the injected `LoggerLike`.
## Features
- Sorts layers ascending by `priority` before merging
- Wraps each layer's `resolve()` in a try/catch and rethrows as
`ConfigError(CONFIG.PARSE_FAILED)`
- Applies `environments[environment]` override after merging (if present)
- Calls `options.validate` if provided; wraps throws as
`ConfigError(CONFIG.VALIDATION_FAILED)`
- Returns a live store with `reload(signal?)` and `subscribe(listener)`
- `reload` re-runs full pipeline; notifies all subscribers on success
- `subscribe` returns an unsubscribe function
- OTel span `config.create` and counter `config.get_total` / `config.reload_total`
## Usage: Basic Example
```ts
import { createConfig, defaultsLayer, envLayer } from '@suaveplan/config';
const store = createConfig({
layers: [
defaultsLayer({ port: 3000, debug: false }),
envLayer({ mapping: { PORT: 'port', DEBUG: 'debug' } }),
],
});
store.get('port'); // value from env or 3000
store.get('debug'); // true/false from env or false
```
## Usage: Advanced Example
```ts
import { createConfig, defaultsLayer, fileLayer, envLayer, cliLayer } from '@suaveplan/config';
import z from 'zod';
const Schema = z.object({
port: z.number().int().min(1).max(65535),
debug: z.boolean(),
db: z.object({ url: z.string().url(), pool: z.number().int().positive() }),
});
type AppConfig = z.infer<typeof Schema>;
const store = createConfig<AppConfig>({
layers: [
defaultsLayer({ port: 3000, debug: false, db: { url: 'postgres://localhost/app', pool: 5 } }),
fileLayer({ content: JSON.parse(readFileSync('./config.json', 'utf-8')) }),
envLayer({ prefix: 'APP_', mapping: { PORT: 'port', DB_URL: 'db.url' } }),
cliLayer({ args: process.argv.slice(2), mapping: { port: 'port', debug: 'debug' } }),
],
validate: (raw) => Schema.parse(raw),
sensitive: ['db.url'],
environments: { production: { debug: false }, development: { debug: true } },
environment: process.env.NODE_ENV ?? 'development',
});
// Live reload on SIGHUP
process.on('SIGHUP', () => store.reload());
// Subscribe to config changes
const unsub = store.subscribe((next) => {
console.log('Config updated, new port:', next.port);
});
```
## API Reference
### `createConfig<T extends ConfigData>(options: CreateConfigOptions<T>): ConfigStore<T>`
| Parameter | Type | Description |
|-----------|------|-------------|
| `options.layers` | `ConfigLayer[]` | Layers to resolve; sorted by priority |
| `options.validate` | `ConfigValidator<T>` | Optional post-merge validator |
| `options.sensitive` | `string[]` | Paths to mask in `toMasked()` |
| `options.environments` | `EnvironmentOverrides<T>` | Per-env overrides |
| `options.environment` | `string` | Active environment name |
| `options.logger` | `LoggerLike` | Optional structured logger |
**Returns** a live `ConfigStore<T>` with real `reload` and `subscribe`.
**Throws:**
- `ConfigError(CONFIG.PARSE_FAILED)` — a layer's `resolve()` threw
- `ConfigError(CONFIG.VALIDATION_FAILED)` — the validator threw
## Implementation Notes
Layer resolution is synchronous — all layer `resolve()` methods must be
synchronous. The `reload()` method is async (returns a `Promise<void>`) to
accommodate future async layer support and to allow AbortSignal handling.
The store's internal `frozenData` reference is atomically swapped after a
successful reload; subscribers see only fully validated data. Partial failures
(e.g. one layer throws during reload) leave the store at the last good state
and propagate the error to the `reload()` caller.

View File

@@ -0,0 +1,114 @@
# config-store — Immutable config store factory for `@suaveplan/config`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createConfigStore` constructs a static, immutable view of a pre-resolved
config object. It deep-freezes the data (including all nested objects) so that
accidental runtime mutations are caught in strict mode. The store provides
`get`, `has`, `toJSON`, and `toMasked` methods for reading config values, plus
no-op stubs for `reload` and `subscribe` that are replaced with live
implementations by `createConfig` when a full config pipeline is constructed.
## Features
- `structuredClone` + `deepFreeze` on construction — data is immutable
- `get(key)` — dot-notation path traversal via `getByPath`
- `has(key)` — presence check via `hasByPath`
- `toJSON()` — returns the frozen data reference
- `toMasked()` — returns a frozen deep clone with sensitive paths replaced
- `reload()` — no-op returning `Promise.resolve()` (stub for static stores)
- `subscribe()` — no-op returning a no-op unsubscribe (stub for static stores)
## Usage: Basic Example
```ts
import { createConfigStore } from '@suaveplan/config/core/config-store/config-store';
const store = createConfigStore({
data: { server: { port: 3000 }, db: { password: 'secret' } },
sensitive: ['db.password'],
});
store.get('server.port'); // 3000
store.has('server.port'); // true
store.has('missing.key'); // false
store.toMasked();
// { server: { port: 3000 }, db: { password: '****' } }
```
## Usage: Advanced Example
```ts
import { createConfigStore } from '@suaveplan/config/core/config-store/config-store';
interface AppConfig {
server: { port: number; host: string };
db: { url: string; pool: number; password: string };
log: { level: string };
}
const store = createConfigStore<AppConfig>({
data: {
server: { port: 4000, host: '0.0.0.0' },
db: { url: 'postgres://...', pool: 10, password: 'hunter2' },
log: { level: 'info' },
},
sensitive: ['db.password', 'db.url'],
});
// JSON-serialise the full config (includes sensitive values — do not log):
JSON.stringify(store.toJSON());
// Safe for logging — sensitive paths are masked:
console.log(store.toMasked());
// { server: ..., db: { url: '****', pool: 10, password: '****' }, log: ... }
```
## API Reference
### `createConfigStore<T extends ConfigData>(options: ConfigStoreOptions<T>): ConfigStore<T>`
| Parameter | Type | Description |
|-----------|------|-------------|
| `options.data` | `T` | Resolved config data |
| `options.sensitive` | `string[]` | Dot-notation paths to mask in `toMasked()` |
**Returns** a `ConfigStore<T>` with all methods implemented.
#### `store.get<K extends string>(key: K): unknown`
Returns the value at the dot-notation `key` path, or `undefined` if not found.
#### `store.has(key: string): boolean`
Returns `true` if the dot-notation path exists and is not `undefined`.
#### `store.toJSON(): Readonly<T>`
Returns the frozen config data reference. This is the full unmasked data; do
not log this value if it contains sensitive fields.
#### `store.toMasked(): Readonly<T>`
Returns a frozen deep clone with all `sensitive` paths replaced by `"****"`.
Safe for logging and telemetry.
#### `store.reload(signal?: AbortSignal): Promise<void>`
No-op stub. Returns `Promise.resolve()`. Live reload is provided by `createConfig`.
#### `store.subscribe(listener): () => void`
No-op stub. Returns a no-op unsubscribe function. Live subscriptions are
provided by `createConfig`.
## Implementation Notes
`deepFreeze` is applied recursively after `structuredClone` to prevent both
shallow and deep mutations. The frozen data object is returned directly from
`toJSON()` (no additional clone) for performance — callers who need a mutable
copy must clone it themselves. `toMasked()` calls `structuredClone` internally
via `maskSensitive`, producing a new mutable copy before re-freezing it.

View File

@@ -0,0 +1,101 @@
# cli-layer — CLI argument config layer for `@suaveplan/config`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`cliLayer` creates a config layer that reads values from command-line arguments
and maps them to dot-notation config paths. It is assigned the highest built-in
priority (`CLI = 40`) so that operator-supplied flags always win over defaults,
file content, environment variables, and programmatic object overrides. The
companion `parseArgs` function handles the full range of common CLI flag forms
and is exported for standalone use in argument-parsing utilities.
## Features
- Priority `40` (`LAYER_PRIORITIES.CLI`) — overrides all other built-in layers
- `parseArgs` handles `--key=value`, `--key value`, `--flag` (boolean), `-k value`
- Unknown flags not in `mapping` are silently ignored
- Values are coerced via `coerceValue` — numerics, booleans, and JSON objects
are automatically converted from their string representations
- Writes to arbitrary dot-notation paths via `setByPath`
## Usage: Basic Example
```ts
import { cliLayer, createConfig } from '@suaveplan/config';
const store = createConfig({
layers: [
cliLayer({
args: process.argv.slice(2),
mapping: { port: 'server.port', debug: 'log.debug' },
}),
],
});
// When invoked as: node app.js --port 9000 --debug
store.get('server.port'); // 9000 (number)
store.get('log.debug'); // true (boolean)
```
## Usage: Advanced Example
```ts
import { parseArgs } from '@suaveplan/config';
// Use parseArgs standalone to inspect raw flag values before coercion:
const raw = parseArgs(
['--host=prod.example.com', '--port', '443', '--tls'],
{ host: 'server.host', port: 'server.port', tls: 'server.tls', p: 'server.port' }
);
// raw = { host: 'prod.example.com', port: '443', tls: 'true' }
// Short flags:
const short = parseArgs(['-p', '8080'], { p: 'server.port' });
// short = { p: '8080' }
```
## API Reference
### `cliLayer(options: CliLayerOptions): ConfigLayer`
| Parameter | Type | Description |
|-----------|------|-------------|
| `options.args` | `string[]` | Raw argv array (e.g. `process.argv.slice(2)`) |
| `options.mapping` | `Record<string, string>` | Flag name → config path map |
**Returns** a `ConfigLayer` with:
- `name`: `"cli"`
- `priority`: `40` (`LAYER_PRIORITIES.CLI`)
- `resolve()`: parses `args`, coerces values, writes to config paths
### `parseArgs(args: string[], mapping: Record<string, string>): Record<string, string>`
Parses raw argv tokens into a flat `{ flagName: rawStringValue }` record.
| Parameter | Type | Description |
|-----------|------|-------------|
| `args` | `string[]` | Raw argument tokens |
| `mapping` | `Record<string, string>` | Allowed flag names (without prefix) |
**Returns** `Record<string, string>` — only flags present in `mapping` are included.
**Supported forms:**
| Form | Example | Key | Value |
|------|---------|-----|-------|
| `--key=value` | `--port=4000` | `port` | `"4000"` |
| `--key value` | `--port 4000` | `port` | `"4000"` |
| `--flag` | `--debug` | `debug` | `"true"` |
| `-k value` | `-p 4000` | `p` | `"4000"` |
## Implementation Notes
Short flags (`-k`) are limited to single-character names. Multi-character short
options (e.g. `-vv`) are not supported and are silently skipped. The parser
does not validate flag values — coercion happens later in `cliLayer.resolve()`
via `coerceValue`. Boolean flags are mapped to the string `"true"` so they
flow through the coercion pipeline consistently.

View File

@@ -0,0 +1,91 @@
# defaults-layer — Static default values layer for `@suaveplan/config`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`defaultsLayer` creates a configuration layer that supplies baseline default
values for every key in the config schema. It is assigned the lowest possible
priority (`LAYER_PRIORITIES.DEFAULTS = 0`) so that every other layer in the
stack — file, object, env, or CLI — can override it without any special
configuration. This makes it the natural first layer to register when building
a `createConfig` call: supply all your application defaults here, and trust that
higher-priority layers will selectively override them.
## Features
- Priority `0` — always the lowest; all other built-in layers override it
- Returns a shallow copy of the provided defaults on `resolve()` — external
mutations to the original object do not affect subsequent resolutions
- Generic `<T extends ConfigData>` so TypeScript can infer the shape
- Named `"defaults"` for clear identification in error messages and logs
## Usage: Basic Example
```ts
import { defaultsLayer, createConfig } from '@suaveplan/config';
interface AppConfig {
port: number;
debug: boolean;
logLevel: string;
}
const store = createConfig<AppConfig>({
layers: [
defaultsLayer<AppConfig>({ port: 3000, debug: false, logLevel: 'info' }),
],
});
store.get('port'); // 3000
store.get('debug'); // false
store.get('logLevel'); // 'info'
```
## Usage: Advanced Example
```ts
import { defaultsLayer, envLayer, cliLayer, createConfig } from '@suaveplan/config';
const defaults = defaultsLayer({
server: { port: 3000, host: '0.0.0.0' },
db: { pool: 5, timeout: 30_000 },
log: { level: 'info', pretty: true },
});
// Override only what differs per environment:
const env = envLayer({
mapping: { PORT: 'server.port', DB_POOL: 'db.pool', LOG_LEVEL: 'log.level' },
});
const cli = cliLayer({
args: process.argv.slice(2),
mapping: { port: 'server.port', debug: 'log.pretty' },
});
const store = createConfig({ layers: [defaults, env, cli] });
```
## API Reference
### `defaultsLayer<T extends ConfigData>(defaults: T): ConfigLayer`
| Parameter | Type | Description |
|-----------|------|-------------|
| `defaults` | `T` | Plain object containing default config values |
**Returns** a `ConfigLayer` with:
- `name`: `"defaults"`
- `priority`: `0` (`LAYER_PRIORITIES.DEFAULTS`)
- `resolve()`: returns `{ ...defaults }` (shallow copy)
## Implementation Notes
The shallow copy returned by `resolve()` prevents the caller from mutating
the internal defaults reference across successive `reload()` calls. For deeply
nested mutable objects, callers should pass plain JSON-serialisable data to
guarantee immutability. The function is intentionally minimal — no async
resolution, no file I/O, no validation — because defaults are static values
known at build time.

View File

@@ -0,0 +1,107 @@
# env-layer — Environment variable config layer for `@suaveplan/config`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`envLayer` creates a config layer that reads environment variables and maps
them to dot-notation config paths. It handles prefix stripping, type coercion
via `coerceValue`, and missing-variable safety (keys absent from the environment
source are simply excluded from the resolved data). With priority
`LAYER_PRIORITIES.ENV = 30`, it overrides defaults, file, and object layers but
is itself overridden by the CLI layer at priority 40.
## Features
- Maps env-var names to arbitrary dot-notation config paths via `mapping`
- Optional `prefix` prepended to each env-var name before lookup
- `source` override for testing without polluting `process.env`
- `coerceValue` exported for standalone use — converts string tokens to the
most specific JS type (boolean, number, parsed JSON, or string)
- Missing or empty env vars are silently skipped (no error thrown)
- Priority `30` — overrides defaults (0), file (10), and object (20) layers
## Usage: Basic Example
```ts
import { envLayer, createConfig } from '@suaveplan/config';
const store = createConfig({
layers: [
envLayer({
mapping: { PORT: 'server.port', DEBUG: 'log.debug' },
}),
],
});
// If PORT=4000 and DEBUG=true are set:
store.get('server.port'); // 4000 (number, not string)
store.get('log.debug'); // true (boolean)
```
## Usage: Advanced Example
```ts
import { envLayer, defaultsLayer, createConfig } from '@suaveplan/config';
// With prefix — all env vars are prefixed with "APP_"
const store = createConfig({
layers: [
defaultsLayer({ server: { port: 3000 }, db: { pool: 5 } }),
envLayer({
prefix: 'APP_',
mapping: {
PORT: 'server.port',
DB_POOL: 'db.pool',
DB_URL: 'db.url',
},
// Can override source for testing:
source: { APP_PORT: '8080', APP_DB_POOL: '20', APP_DB_URL: '{"host":"db"}' },
}),
],
});
store.get('server.port'); // 8080 (number)
store.get('db.pool'); // 20 (number)
store.get('db.url'); // { host: 'db' } (parsed JSON)
```
## API Reference
### `envLayer(options: EnvLayerOptions): ConfigLayer`
| Parameter | Type | Description |
|-----------|------|-------------|
| `options.mapping` | `Record<string, string>` | Env-var name → config path map |
| `options.prefix` | `string` | Optional prefix prepended to each env-var name |
| `options.source` | `Record<string, string \| undefined>` | Override for `process.env` |
| `options.watchBus` | `EventBusLike` | Optional watch bus (reserved for future use) |
| `options.watchEvent` | `string` | Watch event name; default `'config:env:changed'` |
**Returns** a `ConfigLayer` with:
- `name`: `"env"`
- `priority`: `30` (`LAYER_PRIORITIES.ENV`)
### `coerceValue(raw: string): unknown`
Converts a raw environment-variable string to the most specific JavaScript type.
| Input | Output |
|-------|--------|
| `""` | `""` (empty string returned as-is) |
| `"true"`, `"1"`, `"yes"`, `"on"`, `"enabled"` | `true` |
| `"false"`, `"0"`, `"no"`, `"off"`, `"disabled"` | `false` |
| Finite numeric string (`"42"`, `"3.14"`) | `number` |
| JSON object/array string (`'{"a":1}'`) | parsed value |
| Anything else | original `string` |
## Implementation Notes
Coercion is case-insensitive for boolean tokens. Numeric coercion uses
`Number()` + `isFinite` guard to avoid accepting `Infinity` or `NaN`.
JSON parsing is attempted only when the trimmed string starts with `{` or `[`
to avoid unnecessary `try/catch` overhead on every string value. The `source`
parameter defaults to `process.env` in Node/Bun environments and falls back to
`{}` in browser environments where `process` is not defined.

View File

@@ -0,0 +1,94 @@
# file-layer — Pre-parsed file content config layer for `@suaveplan/config`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`fileLayer` creates a config layer that serves pre-parsed configuration data
read from a file. The layer itself does not perform file I/O — that is the
caller's responsibility. This separation keeps the layer synchronous and
testable without file-system access. When an optional `watchBus` event bus is
provided, the layer subscribes to a configurable event name and triggers a
parent-store reload when the event fires, enabling live config reloading
without polling.
## Features
- Priority `10` (`LAYER_PRIORITIES.FILE`) — above defaults, below object/env/CLI
- Serves `structuredClone`-safe shallow copy of provided `content` on `resolve()`
- Optional watch mode via `EventBusLike` + configurable event name
- `teardown()` method unsubscribes the watch listener to prevent memory leaks
- File I/O is the caller's responsibility; this layer is format-agnostic
## Usage: Basic Example
```ts
import { fileLayer, createConfig } from '@suaveplan/config';
import { readFileSync } from 'node:fs';
const content = JSON.parse(readFileSync('./config.json', 'utf-8'));
const store = createConfig({
layers: [fileLayer({ content })],
});
store.get('server.port'); // value from config.json
```
## Usage: Advanced Example
```ts
import { fileLayer, defaultsLayer, createConfig } from '@suaveplan/config';
import { EventBus } from '@suaveplan/events';
import { readFileSync } from 'node:fs';
import { watch } from 'node:fs';
let content = JSON.parse(readFileSync('./app.json', 'utf-8'));
const watchBus = new EventBus();
// Re-parse the file when it changes and signal the bus
watch('./app.json', () => {
content = JSON.parse(readFileSync('./app.json', 'utf-8'));
watchBus.emit('config:file:changed', {});
});
const fileL = fileLayer({
content,
watchBus,
watchEvent: 'config:file:changed',
});
const store = createConfig({
layers: [defaultsLayer({ debug: false }), fileL],
});
// Reloads automatically when app.json changes
await store.reload();
```
## API Reference
### `fileLayer(options: FileLayerOptions): ConfigLayer`
| Parameter | Type | Description |
|-----------|------|-------------|
| `options.content` | `ConfigData` | Pre-parsed config object |
| `options.watchBus` | `EventBusLike` | Optional event bus for watch mode |
| `options.watchEvent` | `string` | Event name to listen for; default `'config:file:changed'` |
**Returns** a `ConfigLayer` with:
- `name`: `"file"`
- `priority`: `10` (`LAYER_PRIORITIES.FILE`)
- `resolve()`: returns `{ ...options.content }` (shallow copy)
- `teardown()`: unsubscribes from the watch bus if one was provided
## Implementation Notes
The watch mechanism is intentionally passive — the layer subscribes to an
event but does not read files itself. The event payload is ignored; only its
arrival matters. The parent `createConfig` live store injects a `_onReload`
callback into the layer after construction to close the loop. If `teardown()`
is not called, the subscription remains active; callers responsible for
application shutdown should ensure all layer teardowns are invoked.

View File

@@ -0,0 +1,83 @@
# object-layer — In-memory config object layer for `@suaveplan/config`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`objectLayer` wraps a plain JavaScript object as a named config layer, making it
easy to inject programmatically constructed configuration into the `createConfig`
pipeline. It is the simplest way to supply test overrides, feature-flag-driven
values, or dynamically computed config blobs without resorting to the full
`fileLayer` or `envLayer` machinery. The default priority (`OBJECT = 20`) places
it above the file layer and defaults layer but below environment variable and CLI
layers.
## Features
- Default priority `20` (`LAYER_PRIORITIES.OBJECT`) — overrides file and defaults
- Configurable priority via the optional second argument
- Returns a shallow copy on `resolve()` for mutation safety
- Named `"object"` for clear identification in logs and error messages
## Usage: Basic Example
```ts
import { objectLayer, createConfig } from '@suaveplan/config';
const store = createConfig({
layers: [
objectLayer({ port: 4000, debug: true }),
],
});
store.get('port'); // 4000
store.get('debug'); // true
```
## Usage: Advanced Example
```ts
import { defaultsLayer, objectLayer, envLayer, createConfig } from '@suaveplan/config';
import { LAYER_PRIORITIES } from '@suaveplan/config/constants';
// Two object layers at different priorities:
const baseObject = objectLayer({ db: { pool: 5, timeout: 30_000 } });
const testOverride = objectLayer(
{ db: { pool: 1 } },
LAYER_PRIORITIES.ENV + 5 // higher than ENV, used in tests
);
const store = createConfig({
layers: [
defaultsLayer({ port: 3000 }),
baseObject,
testOverride,
],
});
store.get('db.pool'); // 1 (test override wins)
```
## API Reference
### `objectLayer(data: ConfigData, priority?: number): ConfigLayer`
| Parameter | Type | Description |
|-----------|------|-------------|
| `data` | `ConfigData` | Plain object to expose as config values |
| `priority` | `number` | Optional priority; defaults to `LAYER_PRIORITIES.OBJECT` (20) |
**Returns** a `ConfigLayer` with:
- `name`: `"object"`
- `priority`: provided or `20`
- `resolve()`: returns `{ ...data }` (shallow copy)
## Implementation Notes
The shallow copy in `resolve()` means that top-level key mutations to the
returned object do not affect subsequent resolutions. Callers who supply deeply
nested mutable objects should be aware that nested references are not deep-cloned.
If immutability at all depths is required, pass `structuredClone(data)` as the
argument. The layer carries no teardown logic since it holds no external resources.

View File

@@ -0,0 +1,181 @@
# common — Public TypeScript types for `@suaveplan/config`
## Purpose
This module defines every public TypeScript interface and type alias used across
the `@suaveplan/config` package. It is the single source of truth for the
package's public API surface, ensuring that all layer implementations, the
config builder, and the config store agree on the same type signatures. By
keeping types in a dedicated file, downstream consumers can import only the
types they need (zero runtime cost) while benefiting from full IntelliSense
support and strict TypeScript narrowing.
## Features
- `ConfigData` — base record type for all config objects
- `ConfigLayer` — contract every layer implementation must satisfy, with optional
`teardown` hook for resource cleanup
- `ConfigValidator<T>` — typed validation/transformation callback
- `EnvironmentOverrides<T>` — per-environment partial override map
- `CreateConfigOptions<T>` — full orchestrator options including `logger` injection
- `ConfigStore<T>` — immutable runtime view with `get`, `has`, `toJSON`, `toMasked`,
`reload`, and `subscribe`
- `ConfigStoreOptions<T>` — low-level static store factory options
- `EnvLayerOptions` — env-var source, prefix, mapping, and optional watch bus
- `CliLayerOptions` — argv array and flag-to-path mapping
- `FileLayerOptions` — pre-parsed content and optional watch bus
## Usage: Basic Example
```ts
import type { ConfigData, ConfigLayer, ConfigStore } from '@suaveplan/config/types/common';
interface AppConfig extends ConfigData {
port: number;
debug: boolean;
}
const layer: ConfigLayer = {
name: 'app-defaults',
priority: 0,
resolve: () => ({ port: 3000, debug: false }),
};
function display(store: ConfigStore<AppConfig>): void {
const port = store.get('port');
console.log(`Listening on port ${port}`);
}
```
## Usage: Advanced Example
```ts
import type {
CreateConfigOptions,
ConfigStore,
ConfigData,
ConfigValidator,
EnvironmentOverrides,
} from '@suaveplan/config/types/common';
import z from 'zod';
const Schema = z.object({ port: z.number(), debug: z.boolean() });
type AppConfig = z.infer<typeof Schema> & ConfigData;
const validate: ConfigValidator<AppConfig> = (raw) => Schema.parse(raw);
const overrides: EnvironmentOverrides<AppConfig> = {
production: { debug: false },
development: { debug: true },
};
const opts: CreateConfigOptions<AppConfig> = {
layers: [],
validate,
sensitive: ['db.password'],
environments: overrides,
environment: process.env.NODE_ENV ?? 'development',
logger: console,
};
```
## API Reference
### `ConfigData`
```ts
type ConfigData = Record<string, unknown>
```
Base type for all config data objects. Allows arbitrary string keys and any
value type, including nested objects.
### `ConfigLayer`
```ts
interface ConfigLayer {
readonly name: string;
readonly priority: number;
resolve(): ConfigData;
teardown?(): void;
}
```
The contract every config layer must implement. `teardown` is optional and is
called when the parent store is torn down (e.g. to unsubscribe from an event bus).
### `ConfigValidator<T>`
```ts
type ConfigValidator<T extends ConfigData = ConfigData> =
(data: ConfigData) => T
```
A synchronous function that validates and transforms raw merged config data. Must
throw on invalid input; the error is caught and wrapped as `CONFIG.VALIDATION_FAILED`.
### `EnvironmentOverrides<T>`
```ts
type EnvironmentOverrides<T extends ConfigData = ConfigData> =
Record<string, Partial<T>>
```
Maps environment names to partial config overrides. Applied on top of the merged
layer data when the active `environment` key matches.
### `CreateConfigOptions<T>`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `layers` | `ConfigLayer[]` | Yes | Layers to resolve and merge |
| `validate` | `ConfigValidator<T>` | No | Post-merge validator |
| `sensitive` | `string[]` | No | Paths to mask in `toMasked()` |
| `environments` | `EnvironmentOverrides<T>` | No | Per-env overrides |
| `environment` | `string` | No | Active environment name |
| `logger` | `LoggerLike` | No | Structured logger for lifecycle events |
### `ConfigStore<T>`
| Method | Returns | Description |
|--------|---------|-------------|
| `get(key)` | `unknown` | Value at dot-notation path, or `undefined` |
| `has(key)` | `boolean` | `true` if path exists |
| `toJSON()` | `Readonly<T>` | Frozen resolved config |
| `toMasked()` | `Readonly<T>` | Config with sensitive paths replaced |
| `reload(signal?)` | `Promise<void>` | Re-resolves all layers |
| `subscribe(fn)` | `() => void` | Listen for reload events; returns unsubscribe |
### `EnvLayerOptions`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mapping` | `Record<string, string>` | Yes | Env-var name → config path |
| `prefix` | `string` | No | Prefix stripped before mapping |
| `source` | `Record<string, string \| undefined>` | No | Override for `process.env` |
| `watchBus` | `EventBusLike` | No | Event bus for watch-mode |
| `watchEvent` | `string` | No | Event name (default: `'config:env:changed'`) |
### `CliLayerOptions`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `args` | `string[]` | Yes | Raw argv (e.g. `process.argv.slice(2)`) |
| `mapping` | `Record<string, string>` | Yes | Flag name → config path |
### `FileLayerOptions`
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | `ConfigData` | Yes | Pre-parsed file content |
| `watchBus` | `EventBusLike` | No | Event bus for watch-mode |
| `watchEvent` | `string` | No | Event name (default: `'config:file:changed'`) |
## Implementation Notes
All types in this module are pure TypeScript interfaces and type aliases — they
produce zero runtime output. The `ConfigLayer.teardown` optional method was added
to support watch-mode layers that hold event-bus subscriptions. The
`ConfigStore.reload` and `ConfigStore.subscribe` methods are optional on the
interface for backward compatibility but are always present on concrete stores
returned by `createConfig`.

View File

@@ -0,0 +1,110 @@
# schemas — Zod option schemas for `@suaveplan/config`
## Purpose
This module exposes Zod schemas for every public options interface in
`@suaveplan/config`. Using these schemas, callers can validate externally
sourced configuration objects at runtime — for instance, when reading layer
options from a database row, an HTTP request body, or an untrusted JSON file.
The schemas complement the TypeScript interfaces in `types/common.ts` by adding
runtime validation without duplicating the structural definitions.
## Features
- `EnvLayerOptionsSchema` — validates `EnvLayerOptions` (mapping required, prefix optional)
- `CliLayerOptionsSchema` — validates `CliLayerOptions` (args array + mapping required)
- `FileLayerOptionsSchema` — validates `FileLayerOptions` (content record required)
- `ConfigStoreOptionsSchema` — validates `ConfigStoreOptions` (data + sensitive array)
- `CreateConfigOptionsSchema` — validates the shape-checkable subset of `CreateConfigOptions`
(functions and loggers are excluded, layer structural fields only)
- Inferred companion types (`EnvLayerOptionsInput`, etc.) for consumers that want
the Zod-inferred type rather than the hand-written TS interface
## Usage: Basic Example
```ts
import { EnvLayerOptionsSchema } from '@suaveplan/config/types/schemas';
const raw = JSON.parse(process.env.ENV_LAYER_CONFIG ?? '{}');
const opts = EnvLayerOptionsSchema.parse(raw);
// opts is now typed as EnvLayerOptionsInput
```
## Usage: Advanced Example
```ts
import { CreateConfigOptionsSchema } from '@suaveplan/config/types/schemas';
import z from 'zod';
// Validate only the serialisable portion of options coming from a config API.
function parseRemoteConfigOptions(body: unknown) {
const result = CreateConfigOptionsSchema.safeParse(body);
if (!result.success) {
throw new Error(`Invalid config options: ${result.error.message}`);
}
return result.data;
}
// Then merge the validated shape with local layer/validate functions before
// passing to createConfig.
```
## API Reference
### `EnvLayerOptionsSchema`
Validates `{ mapping: Record<string, string>, prefix?: string, source?: Record<string, string|undefined>, watchEvent?: string }`.
- `mapping` — required; record of env-var names to dot-notation config paths
- `prefix` — optional string stripped from env-var names before mapping
- `source` — optional environment source override
- `watchEvent` — optional event name for watch-mode
**Inferred type**: `EnvLayerOptionsInput`
### `CliLayerOptionsSchema`
Validates `{ args: string[], mapping: Record<string, string> }`.
- `args` — required array of raw CLI arguments
- `mapping` — required; record of flag names to config paths
**Inferred type**: `CliLayerOptionsInput`
### `FileLayerOptionsSchema`
Validates `{ content: Record<string, unknown>, watchEvent?: string }`.
- `content` — required; pre-parsed file content
- `watchEvent` — optional event name for watch-mode
**Inferred type**: `FileLayerOptionsInput`
### `ConfigStoreOptionsSchema`
Validates `{ data: Record<string, unknown>, sensitive: string[] }`.
- `data` — required; the resolved config data
- `sensitive` — required; list of dot-notation paths to mask
**Inferred type**: `ConfigStoreOptionsInput`
### `CreateConfigOptionsSchema`
Validates the serialisable subset of `CreateConfigOptions`:
`{ layers: Array<{ name: string, priority: number }>, sensitive?: string[], environments?: Record<string, Record<string, unknown>>, environment?: string }`.
Functions (`validate`, `logger`) and layer `resolve` methods are intentionally
excluded — they cannot be serialised to JSON or validated with Zod.
**Inferred type**: `CreateConfigOptionsInput`
## Implementation Notes
The `CreateConfigOptionsSchema` deliberately omits the `validate` callback and
`logger` fields from the Zod schema. These are functions and cannot be round-
tripped through a serialisation boundary. Callers who build `CreateConfigOptions`
programmatically should merge the validated structural fields with their local
function references before calling `createConfig`. The `watchBus` fields on
`EnvLayerOptionsSchema` and `FileLayerOptionsSchema` are also excluded because
`EventBusLike` instances are not serialisable; pass them directly in code.

View File

@@ -0,0 +1,115 @@
# mask — Path utilities and sensitive-key masking for `@suaveplan/config`
## Purpose
This module provides low-level dot-notation path traversal utilities
(`getByPath`, `setByPath`, `hasByPath`) and the `maskSensitive` function that
uses them to redact sensitive keys from config snapshots before they are logged
or transmitted. The path utilities are also used internally by `envLayer` and
`cliLayer` to write coerced values into the result object without requiring
the caller to construct deeply nested structures manually.
## Features
- `getByPath` — safely reads a value at any dot-notation depth; returns `undefined`
on any missing or non-object intermediate segment
- `hasByPath` — `true` if `getByPath` resolves to a non-`undefined` value
- `setByPath` — writes a value at a dot-notation path, creating intermediate objects
as needed; mutates the target and returns it
- `maskSensitive` — `structuredClone` + `setByPath` each sensitive path with
`MASK_STRING` (`"****"`) or a custom mask string
## Usage: Basic Example
```ts
import { getByPath, setByPath, maskSensitive } from '@suaveplan/config/utils/mask';
const cfg = { db: { host: 'localhost', password: 'secret' } };
getByPath(cfg, 'db.host'); // 'localhost'
getByPath(cfg, 'db.missing'); // undefined
const masked = maskSensitive(cfg, ['db.password']);
// { db: { host: 'localhost', password: '****' } }
// cfg is unchanged
```
## Usage: Advanced Example
```ts
import { setByPath, getByPath, maskSensitive } from '@suaveplan/config/utils/mask';
// Build a config object from flat CLI flags via dot paths
const result = {};
setByPath(result, 'server.port', 4000);
setByPath(result, 'server.tls.enabled', true);
setByPath(result, 'db.password', 'hunter2');
// result = { server: { port: 4000, tls: { enabled: true } }, db: { password: 'hunter2' } }
// Multi-path masking with custom mask string
const safe = maskSensitive(result, ['db.password', 'server.tls.enabled'], '[REDACTED]');
// { server: { port: 4000, tls: { enabled: '[REDACTED]' } }, db: { password: '[REDACTED]' } }
```
## API Reference
### `getByPath(obj: ConfigData, path: string): unknown`
Traverses `obj` by splitting `path` on `"."` and descending at each segment.
| Parameter | Type | Description |
|-----------|------|-------------|
| `obj` | `ConfigData` | Root object to traverse |
| `path` | `string` | Dot-notation path, e.g. `"db.host"` |
**Returns** the resolved value or `undefined` if any segment is missing or an
intermediate value is not a plain object (e.g. is `null`, an array, or a scalar).
### `hasByPath(obj: ConfigData, path: string): boolean`
Returns `getByPath(obj, path) !== undefined`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `obj` | `ConfigData` | Root object |
| `path` | `string` | Dot-notation path |
### `setByPath(obj: ConfigData, path: string, value: unknown): ConfigData`
Writes `value` at the specified dot-notation path, creating intermediate plain
objects as needed. Mutates `obj` in place.
| Parameter | Type | Description |
|-----------|------|-------------|
| `obj` | `ConfigData` | Root object to mutate |
| `path` | `string` | Dot-notation path |
| `value` | `unknown` | Value to write |
**Returns** `obj` (same reference, mutated).
**Side effect:** Any intermediate segment that is not a plain object (null,
array, scalar) is replaced with a new `{}`.
### `maskSensitive(data: ConfigData, sensitive: string[], mask?: string): ConfigData`
Returns a deep clone of `data` with every path in `sensitive` replaced by
`mask`. Does not modify the original `data`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `data` | `ConfigData` | Source config object |
| `sensitive` | `string[]` | Dot-notation paths to redact |
| `mask` | `string` | Replacement string; defaults to `"****"` |
**Returns** a new `ConfigData` with sensitive values replaced. Paths that do
not exist in `data` are silently skipped.
## Implementation Notes
`structuredClone` is used in `maskSensitive` to produce a deep copy before
mutation. This is safe for all JSON-serialisable values (the only kind that
should appear in config data). The function is idempotent: applying it twice
with the same arguments yields the same result as applying it once, because the
mask string `"****"` is itself a valid value that survives the next clone-and-
replace cycle unchanged.

View File

@@ -0,0 +1,90 @@
# merge — Deep-merge and layer-folding utilities for `@suaveplan/config`
## Purpose
This module provides two functions that form the merge backbone of the config
pipeline: `deepMerge` performs a structural recursive merge of two plain-object
trees, and `mergeLayers` folds an ordered array of named config layers using
`deepMerge`. These utilities are deliberately small and self-contained — they
carry no external runtime dependencies beyond the OTel API no-op SDK, and are
therefore safe to import in any environment.
## Features
- `deepMerge` — recursively merges two plain objects; arrays replaced, `undefined`
override values skipped, non-object scalars overwrite
- `mergeLayers` — folds a pre-sorted list of `{ name, data }` entries using
`deepMerge`; records merge duration to `config.merge_duration_ms` OTel histogram
- No mutation of input objects — both functions return new objects
- TypeScript generics preserve the inferred type of the base object
## Usage: Basic Example
```ts
import { deepMerge } from '@suaveplan/config/utils/merge';
const base = { port: 3000, db: { host: 'localhost', port: 5432 } };
const override = { db: { host: 'prod.db.example.com' } };
const merged = deepMerge(base, override);
// { port: 3000, db: { host: 'prod.db.example.com', port: 5432 } }
```
## Usage: Advanced Example
```ts
import { mergeLayers } from '@suaveplan/config/utils/merge';
const layers = [
{ name: 'defaults', data: { port: 3000, debug: true, db: { pool: 5 } } },
{ name: 'file', data: { port: 4000, db: { pool: 10 } } },
{ name: 'env', data: { debug: false } },
];
const result = mergeLayers(layers);
// { port: 4000, debug: false, db: { pool: 10 } }
// Note: layers must be pre-sorted by priority — mergeLayers does not sort.
```
## API Reference
### `deepMerge<T extends ConfigData>(base: T, override: Partial<T>): T`
Recursively merges `override` into `base` and returns a new object.
| Parameter | Type | Description |
|-----------|------|-------------|
| `base` | `T` | The base object to merge into |
| `override` | `Partial<T>` | Values to apply on top of `base` |
**Returns** a new `T`-typed object; neither argument is mutated.
**Merge rules:**
- Plain objects (non-array, non-null, prototype === `Object.prototype`) are merged recursively.
- Arrays in `override` replace the corresponding value in `base`.
- Keys with `undefined` values in `override` are skipped (base value preserved).
- All other scalar values in `override` overwrite the base value.
### `mergeLayers(layers: Array<{ name: string; data: ConfigData }>): ConfigData`
Folds `layers` left-to-right using `deepMerge`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `layers` | `Array<{ name: string; data: ConfigData }>` | Pre-sorted layer descriptors |
**Returns** the merged `ConfigData`. Layers must be sorted by priority before
calling this function; `mergeLayers` itself does not sort.
**OTel:** Records the total duration to the `config.merge_duration_ms` histogram
with a `config.layer_count` attribute. This is automatically a no-op when no OTel
SDK is installed.
## Implementation Notes
`isPlainObject` guards against merging class instances, `Date` objects, `Map`,
`Set`, and other non-plain-object values — those are replaced rather than merged.
This is intentional: config values should be JSON-serialisable plain data. The
`deepMerge` function does not handle circular references; config objects are
assumed to be acyclic (a reasonable constraint for any serialisable config tree).