@rawnodes/config-loader 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 RawNodes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # @rawnodes/config-loader
2
+
3
+ Flexible YAML configuration loader for Node.js applications with environment overrides, Zod validation, and Docker-friendly features.
4
+
5
+ ## Features
6
+
7
+ - **YAML Configuration** - Load config from YAML files with environment-specific overrides
8
+ - **Environment Variables** - Replace `${VAR}` placeholders with env values
9
+ - **Zod Validation** - Optional schema validation with detailed error messages
10
+ - **Docker/K8s Ready** - Mount additional config files via `overrideDir`
11
+ - **Secret Masking** - Automatic masking of sensitive values in logs
12
+ - **TypeScript First** - Full type safety with generics
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add @rawnodes/config-loader
18
+ # or
19
+ npm install @rawnodes/config-loader
20
+ ```
21
+
22
+ For Zod validation (optional):
23
+ ```bash
24
+ pnpm add zod
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### 1. Create config files
30
+
31
+ ```yaml
32
+ # config/base.yml
33
+ server:
34
+ port: 3000
35
+ host: localhost
36
+
37
+ database:
38
+ host: localhost
39
+ port: 5432
40
+ name: myapp
41
+ ```
42
+
43
+ ```yaml
44
+ # config/production.yml
45
+ server:
46
+ host: 0.0.0.0
47
+
48
+ database:
49
+ host: ${DATABASE_HOST}
50
+ password: ${DATABASE_PASSWORD}
51
+ ```
52
+
53
+ ### 2. Load configuration
54
+
55
+ ```typescript
56
+ import { loadConfig } from '@rawnodes/config-loader';
57
+
58
+ interface AppConfig {
59
+ server: { port: number; host: string };
60
+ database: { host: string; port: number; name: string; password?: string };
61
+ }
62
+
63
+ const { config } = loadConfig<AppConfig>({
64
+ configDir: './config',
65
+ dotenv: true,
66
+ });
67
+
68
+ console.log(config.server.port); // 3000
69
+ ```
70
+
71
+ ## Configuration Options
72
+
73
+ ```typescript
74
+ interface ConfigLoaderOptions<T> {
75
+ // Directory with config files (default: process.cwd())
76
+ configDir?: string;
77
+
78
+ // Base config filename without extension (default: 'base')
79
+ baseFileName?: string;
80
+
81
+ // Environment name (default: process.env.NODE_ENV || 'local')
82
+ environment?: string;
83
+
84
+ // File extension (default: 'yml')
85
+ extension?: 'yml' | 'yaml';
86
+
87
+ // Custom post-processing function
88
+ postProcess?: (config: T) => T;
89
+
90
+ // Zod schema for validation
91
+ schema?: z.ZodType<T>;
92
+
93
+ // Logger callback (config will be logged with masked secrets)
94
+ logger?: (message: string) => void;
95
+
96
+ // Load .env file (default: false)
97
+ dotenv?: boolean | { path?: string };
98
+
99
+ // Directory with additional YAML files to merge (default: '/etc/app/config')
100
+ // Set to false to disable
101
+ overrideDir?: string | false;
102
+ }
103
+ ```
104
+
105
+ ## Environment Variables
106
+
107
+ Use `${VAR}` syntax with optional defaults:
108
+
109
+ ```yaml
110
+ database:
111
+ host: ${DB_HOST:localhost}
112
+ port: ${DB_PORT:5432}
113
+ password: ${DB_PASSWORD} # Required - throws if not set
114
+ ```
115
+
116
+ ## Zod Validation
117
+
118
+ ```typescript
119
+ import { z } from 'zod';
120
+ import { loadConfig } from '@rawnodes/config-loader';
121
+
122
+ const AppConfigSchema = z.object({
123
+ server: z.object({
124
+ port: z.number().min(1).max(65535),
125
+ host: z.string(),
126
+ }),
127
+ database: z.object({
128
+ host: z.string(),
129
+ port: z.number(),
130
+ name: z.string(),
131
+ password: z.string().optional(),
132
+ }),
133
+ });
134
+
135
+ type AppConfig = z.infer<typeof AppConfigSchema>;
136
+
137
+ const { config } = loadConfig<AppConfig>({
138
+ schema: AppConfigSchema,
139
+ logger: console.log,
140
+ });
141
+ ```
142
+
143
+ ## Docker/Kubernetes Override
144
+
145
+ Mount additional config files in `/etc/app/config/`:
146
+
147
+ ```yaml
148
+ # /etc/app/config/01-secrets.yml
149
+ database:
150
+ password: super-secret
151
+
152
+ # /etc/app/config/02-overrides.yml
153
+ server:
154
+ port: 8080
155
+ ```
156
+
157
+ Files are merged in alphabetical order.
158
+
159
+ ## NestJS Integration
160
+
161
+ ```typescript
162
+ // config.module.ts
163
+ import { Module, Global } from '@nestjs/common';
164
+ import { loadConfig } from '@rawnodes/config-loader';
165
+ import { AppConfigSchema, AppConfig } from './app.config';
166
+
167
+ const { config } = loadConfig<AppConfig>({
168
+ schema: AppConfigSchema,
169
+ dotenv: true,
170
+ });
171
+
172
+ @Global()
173
+ @Module({
174
+ providers: [
175
+ {
176
+ provide: 'CONFIG',
177
+ useValue: config,
178
+ },
179
+ ],
180
+ exports: ['CONFIG'],
181
+ })
182
+ export class ConfigModule {}
183
+ ```
184
+
185
+ ## API
186
+
187
+ ### `loadConfig<T>(options?): ConfigLoaderResult<T>`
188
+
189
+ Loads and merges configuration files.
190
+
191
+ **Returns:**
192
+ ```typescript
193
+ interface ConfigLoaderResult<T> {
194
+ config: T; // Loaded configuration
195
+ environment: string; // Resolved environment name
196
+ configDir: string; // Resolved config directory path
197
+ }
198
+ ```
199
+
200
+ ### `maskSecrets(obj): unknown`
201
+
202
+ Masks sensitive values in an object. Useful for logging.
203
+
204
+ ```typescript
205
+ import { maskSecrets } from '@rawnodes/config-loader';
206
+
207
+ const masked = maskSecrets({
208
+ user: 'admin',
209
+ password: 'secret123',
210
+ url: 'postgres://user:pass@localhost/db',
211
+ });
212
+ // { user: 'admin', password: 'se***23', url: 'postgres://user:***@localhost/db' }
213
+ ```
214
+
215
+ ### `deepMerge(base, override): object`
216
+
217
+ Deep merges two objects.
218
+
219
+ ### `replacePlaceholders(obj): unknown`
220
+
221
+ Replaces `${VAR}` placeholders with environment variable values.
222
+
223
+ ## License
224
+
225
+ MIT
@@ -0,0 +1,4 @@
1
+ export { loadConfig } from './loader.js';
2
+ export { deepMerge, replacePlaceholders, maskSecrets } from './utils/index.js';
3
+ export type { ConfigLoaderOptions, ConfigLoaderResult, DotenvOptions, } from './types.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/E,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,GACd,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { loadConfig } from './loader.js';
2
+ export { deepMerge, replacePlaceholders, maskSecrets } from './utils/index.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ConfigLoaderOptions, ConfigLoaderResult } from './types.js';
2
+ export declare function loadConfig<T>(options?: ConfigLoaderOptions<T>): ConfigLoaderResult<T>;
3
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAW1E,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAE,mBAAmB,CAAC,CAAC,CAAM,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAuDzF"}
package/dist/loader.js ADDED
@@ -0,0 +1,100 @@
1
+ import { readFileSync, existsSync, readdirSync } from 'fs';
2
+ import * as yaml from 'js-yaml';
3
+ import * as dotenv from 'dotenv';
4
+ import { join } from 'path';
5
+ import { deepMerge, replacePlaceholders, maskSecrets } from './utils/index.js';
6
+ const DEFAULT_OPTIONS = {
7
+ configDir: process.cwd(),
8
+ baseFileName: 'base',
9
+ environment: process.env.NODE_ENV || 'local',
10
+ extension: 'yml',
11
+ overrideDir: '/etc/app/config',
12
+ };
13
+ export function loadConfig(options = {}) {
14
+ const opts = { ...DEFAULT_OPTIONS, ...options };
15
+ const { configDir, baseFileName, environment, extension } = opts;
16
+ // Load .env if requested
17
+ if (options.dotenv) {
18
+ loadDotenv(options.dotenv, environment);
19
+ }
20
+ const resolvedConfigDir = resolveConfigDir(configDir);
21
+ const baseConfig = loadYamlFile(resolvedConfigDir, `${baseFileName}.${extension}`);
22
+ const envConfig = loadYamlFile(resolvedConfigDir, `${environment}.${extension}`);
23
+ let mergedConfig = deepMerge(baseConfig, envConfig);
24
+ // Load override files from directory
25
+ const overrideDir = options.overrideDir !== false ? (options.overrideDir ?? DEFAULT_OPTIONS.overrideDir) : null;
26
+ if (overrideDir) {
27
+ const overrideConfigs = loadOverrideDir(overrideDir);
28
+ for (const override of overrideConfigs) {
29
+ mergedConfig = deepMerge(mergedConfig, override);
30
+ }
31
+ }
32
+ let config = replacePlaceholders(mergedConfig);
33
+ // Post-process
34
+ if (options.postProcess) {
35
+ config = options.postProcess(config);
36
+ }
37
+ // Validate with Zod schema
38
+ if (options.schema) {
39
+ const result = options.schema.safeParse(config);
40
+ if (!result.success) {
41
+ const errors = result.error.issues
42
+ .map((e) => ` - ${e.path.join('.')}: ${e.message}`)
43
+ .join('\n');
44
+ throw new Error(`Config validation failed:\n${errors}`);
45
+ }
46
+ config = result.data;
47
+ }
48
+ // Log config with masked secrets
49
+ if (options.logger) {
50
+ const maskedConfig = maskSecrets(config);
51
+ options.logger(`Config loaded (${environment}):\n${JSON.stringify(maskedConfig, null, 2)}`);
52
+ }
53
+ return {
54
+ config,
55
+ environment,
56
+ configDir: resolvedConfigDir,
57
+ };
58
+ }
59
+ function loadDotenv(dotenvOption, environment) {
60
+ let envPath;
61
+ if (typeof dotenvOption === 'object' && dotenvOption.path) {
62
+ envPath = dotenvOption.path;
63
+ }
64
+ else {
65
+ envPath = environment === 'production' ? '.env' : `.env.${environment}`;
66
+ }
67
+ dotenv.config({ path: envPath });
68
+ }
69
+ function resolveConfigDir(configDir) {
70
+ const possiblePaths = [
71
+ join(configDir, 'src', 'config'),
72
+ join(configDir, 'dist', 'config'),
73
+ join(configDir, 'config'),
74
+ configDir,
75
+ ];
76
+ for (const path of possiblePaths) {
77
+ if (existsSync(join(path, 'base.yml')) || existsSync(join(path, 'base.yaml'))) {
78
+ return path;
79
+ }
80
+ }
81
+ throw new Error(`Config files not found. Searched in:\n${possiblePaths.map((p) => ` - ${p}`).join('\n')}`);
82
+ }
83
+ function loadYamlFile(dir, filename) {
84
+ const filePath = join(dir, filename);
85
+ if (!existsSync(filePath)) {
86
+ return {};
87
+ }
88
+ const content = readFileSync(filePath, 'utf8');
89
+ return yaml.load(content) || {};
90
+ }
91
+ function loadOverrideDir(dir) {
92
+ if (!existsSync(dir)) {
93
+ return [];
94
+ }
95
+ const files = readdirSync(dir)
96
+ .filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'))
97
+ .sort();
98
+ return files.map((file) => loadYamlFile(dir, file));
99
+ }
100
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ import type { z } from 'zod';
2
+ export interface DotenvOptions {
3
+ /**
4
+ * Path to .env file
5
+ * @default auto-detected based on NODE_ENV
6
+ */
7
+ path?: string;
8
+ }
9
+ export interface ConfigLoaderOptions<T = unknown> {
10
+ /**
11
+ * Directory where config files are located
12
+ * @default process.cwd()
13
+ */
14
+ configDir?: string;
15
+ /**
16
+ * Base config filename (without extension)
17
+ * @default 'base'
18
+ */
19
+ baseFileName?: string;
20
+ /**
21
+ * Environment name for environment-specific config
22
+ * @default process.env.NODE_ENV || 'local'
23
+ */
24
+ environment?: string;
25
+ /**
26
+ * File extension
27
+ * @default 'yml'
28
+ */
29
+ extension?: 'yml' | 'yaml';
30
+ /**
31
+ * Custom post-processing function to transform the loaded config
32
+ */
33
+ postProcess?: (config: T) => T;
34
+ /**
35
+ * Zod schema for validation
36
+ * If provided, config will be validated against this schema
37
+ */
38
+ schema?: z.ZodType<T>;
39
+ /**
40
+ * Logger callback for logging loaded config
41
+ * Config will be logged with secrets masked
42
+ */
43
+ logger?: (message: string) => void;
44
+ /**
45
+ * Load .env file before loading config
46
+ * - true: auto-detect .env file based on NODE_ENV
47
+ * - object: custom options
48
+ */
49
+ dotenv?: boolean | DotenvOptions;
50
+ /**
51
+ * Directory with additional YAML files to merge
52
+ * All *.yml files in this directory will be merged (sorted by filename)
53
+ * Useful for Docker/Kubernetes config mounts
54
+ * @default '/etc/app/config'
55
+ */
56
+ overrideDir?: string | false;
57
+ }
58
+ export interface ConfigLoaderResult<T> {
59
+ config: T;
60
+ environment: string;
61
+ configDir: string;
62
+ }
63
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC,GAAG,OAAO;IAC9C;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IAE3B;;OAEG;IACH,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IAE/B;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAEtB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAEnC;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAEjC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,MAAM,EAAE,CAAC,CAAC;IACV,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ type DeepObject = Record<string, unknown>;
2
+ export declare function deepMerge(base: DeepObject, override: DeepObject): DeepObject;
3
+ export {};
4
+ //# sourceMappingURL=deep-merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep-merge.d.ts","sourceRoot":"","sources":["../../src/utils/deep-merge.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,GAAG,UAAU,CAe5E"}
@@ -0,0 +1,18 @@
1
+ export function deepMerge(base, override) {
2
+ const merged = { ...base };
3
+ for (const key in override) {
4
+ const overrideValue = override[key];
5
+ const baseValue = base[key];
6
+ if (isPlainObject(overrideValue) && isPlainObject(baseValue)) {
7
+ merged[key] = deepMerge(baseValue, overrideValue);
8
+ }
9
+ else {
10
+ merged[key] = overrideValue;
11
+ }
12
+ }
13
+ return merged;
14
+ }
15
+ function isPlainObject(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
18
+ //# sourceMappingURL=deep-merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep-merge.js","sourceRoot":"","sources":["../../src/utils/deep-merge.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,SAAS,CAAC,IAAgB,EAAE,QAAoB;IAC9D,MAAM,MAAM,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC;IAE3B,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,MAAM,aAAa,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QAE5B,IAAI,aAAa,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7D,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,SAAuB,EAAE,aAA2B,CAAC,CAAC;QAChF,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function replacePlaceholders(obj: unknown): unknown;
2
+ //# sourceMappingURL=env-replacer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-replacer.d.ts","sourceRoot":"","sources":["../../src/utils/env-replacer.ts"],"names":[],"mappings":"AAEA,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAkBzD"}
@@ -0,0 +1,42 @@
1
+ const ENV_PLACEHOLDER_REGEX = /\${(.*?)}/g;
2
+ export function replacePlaceholders(obj) {
3
+ if (typeof obj === 'string') {
4
+ return replaceStringPlaceholders(obj);
5
+ }
6
+ if (Array.isArray(obj)) {
7
+ return obj.map((item) => replacePlaceholders(item));
8
+ }
9
+ if (isPlainObject(obj)) {
10
+ const result = {};
11
+ for (const key of Object.keys(obj)) {
12
+ result[key] = replacePlaceholders(obj[key]);
13
+ }
14
+ return result;
15
+ }
16
+ return obj;
17
+ }
18
+ function replaceStringPlaceholders(str) {
19
+ return str.replace(ENV_PLACEHOLDER_REGEX, (match, key) => {
20
+ const [envKey, ...rest] = key.split(':');
21
+ const defaultValue = rest.length > 0 ? rest.join(':') : undefined;
22
+ const envValue = process.env[envKey];
23
+ let value;
24
+ if (envValue !== undefined) {
25
+ value = envValue;
26
+ }
27
+ else if (defaultValue !== undefined) {
28
+ value = defaultValue;
29
+ }
30
+ else {
31
+ throw new Error(`Environment variable "${envKey}" is not defined and no default value provided`);
32
+ }
33
+ if (value.includes('${')) {
34
+ return replacePlaceholders(value);
35
+ }
36
+ return value;
37
+ });
38
+ }
39
+ function isPlainObject(value) {
40
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
41
+ }
42
+ //# sourceMappingURL=env-replacer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-replacer.js","sourceRoot":"","sources":["../../src/utils/env-replacer.ts"],"names":[],"mappings":"AAAA,MAAM,qBAAqB,GAAG,YAAY,CAAC;AAE3C,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,yBAAyB,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC;IACtD,CAAC;IAED,IAAI,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACnC,MAAM,CAAC,GAAG,CAAC,GAAG,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,yBAAyB,CAAC,GAAW;IAC5C,OAAO,GAAG,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvD,MAAM,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACzC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAClE,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAErC,IAAI,KAAa,CAAC;QAClB,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,KAAK,GAAG,QAAQ,CAAC;QACnB,CAAC;aAAM,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YACtC,KAAK,GAAG,YAAY,CAAC;QACvB,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,yBAAyB,MAAM,gDAAgD,CAAC,CAAC;QACnG,CAAC;QAED,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,OAAO,mBAAmB,CAAC,KAAK,CAAW,CAAC;QAC9C,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { deepMerge } from './deep-merge.js';
2
+ export { replacePlaceholders } from './env-replacer.js';
3
+ export { maskSecrets } from './mask-secrets.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { deepMerge } from './deep-merge.js';
2
+ export { replacePlaceholders } from './env-replacer.js';
3
+ export { maskSecrets } from './mask-secrets.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function maskSecrets(obj: unknown): unknown;
2
+ //# sourceMappingURL=mask-secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mask-secrets.d.ts","sourceRoot":"","sources":["../../src/utils/mask-secrets.ts"],"names":[],"mappings":"AAIA,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAuBjD"}
@@ -0,0 +1,49 @@
1
+ const SECRET_PATTERNS = ['token', 'password', 'secret', 'key', 'apikey', 'api_key', 'credential'];
2
+ const URL_WITH_CREDENTIALS_REGEX = /^([a-z]+:\/\/)([^:]+):([^@]+)@(.+)$/i;
3
+ export function maskSecrets(obj) {
4
+ if (typeof obj === 'string') {
5
+ return maskUrlCredentials(obj);
6
+ }
7
+ if (Array.isArray(obj)) {
8
+ return obj.map((item) => maskSecrets(item));
9
+ }
10
+ if (isPlainObject(obj)) {
11
+ const result = {};
12
+ for (const key of Object.keys(obj)) {
13
+ const value = obj[key];
14
+ if (isSecretKey(key)) {
15
+ result[key] = maskValue(value);
16
+ }
17
+ else {
18
+ result[key] = maskSecrets(value);
19
+ }
20
+ }
21
+ return result;
22
+ }
23
+ return obj;
24
+ }
25
+ function isSecretKey(key) {
26
+ const lowerKey = key.toLowerCase();
27
+ return SECRET_PATTERNS.some((pattern) => lowerKey.includes(pattern));
28
+ }
29
+ function maskValue(value) {
30
+ if (typeof value !== 'string') {
31
+ return '***';
32
+ }
33
+ if (value.length <= 4) {
34
+ return '***';
35
+ }
36
+ return value.slice(0, 2) + '***' + value.slice(-2);
37
+ }
38
+ function maskUrlCredentials(url) {
39
+ const match = url.match(URL_WITH_CREDENTIALS_REGEX);
40
+ if (match) {
41
+ const [, protocol, user, , host] = match;
42
+ return `${protocol}${user}:***@${host}`;
43
+ }
44
+ return url;
45
+ }
46
+ function isPlainObject(value) {
47
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
48
+ }
49
+ //# sourceMappingURL=mask-secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mask-secrets.js","sourceRoot":"","sources":["../../src/utils/mask-secrets.ts"],"names":[],"mappings":"AAAA,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;AAElG,MAAM,0BAA0B,GAAG,sCAAsC,CAAC;AAE1E,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,IAAI,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YACvB,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrB,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YACjC,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;IACnC,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,SAAS,kBAAkB,CAAC,GAAW;IACrC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;IACpD,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,AAAD,EAAG,IAAI,CAAC,GAAG,KAAK,CAAC;QACzC,OAAO,GAAG,QAAQ,GAAG,IAAI,QAAQ,IAAI,EAAE,CAAC;IAC1C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@rawnodes/config-loader",
3
+ "version": "1.0.0",
4
+ "description": "Flexible YAML config loader with environment overrides, Zod validation, and Docker-friendly features",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsc --watch",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "test:coverage": "vitest run --coverage",
25
+ "lint": "eslint src --ext .ts",
26
+ "prepublishOnly": "pnpm build"
27
+ },
28
+ "keywords": [
29
+ "config",
30
+ "configuration",
31
+ "yaml",
32
+ "environment",
33
+ "dotenv",
34
+ "zod",
35
+ "typescript",
36
+ "docker",
37
+ "kubernetes"
38
+ ],
39
+ "author": "RawNodes",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/rawnodes/config-loader.git"
44
+ },
45
+ "engines": {
46
+ "node": ">=18"
47
+ },
48
+ "dependencies": {
49
+ "dotenv": "^16.4.5",
50
+ "js-yaml": "^4.1.0"
51
+ },
52
+ "peerDependencies": {
53
+ "zod": "^3.0.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "zod": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@types/js-yaml": "^4.0.9",
62
+ "@types/node": "^22.0.0",
63
+ "typescript": "^5.7.0",
64
+ "vitest": "^2.0.0",
65
+ "zod": "^3.24.0"
66
+ }
67
+ }