@rawnodes/config-loader 1.3.0 → 1.5.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/README.md CHANGED
@@ -6,6 +6,8 @@ Flexible YAML configuration loader for Node.js applications with environment ove
6
6
 
7
7
  - **YAML Configuration** - Load config from YAML files with environment-specific overrides
8
8
  - **Environment Variables** - Replace `${VAR}` placeholders with env values
9
+ - **HashiCorp Vault** - Read secrets from Vault using `${vault:path:field}` syntax
10
+ - **AWS Secrets Manager** - Read secrets from AWS using `${aws:secret-name:field}` syntax
9
11
  - **Zod Validation** - Optional schema validation with detailed error messages
10
12
  - **Docker/K8s Ready** - Mount additional config files via `overrideDir`
11
13
  - **Secret Masking** - Automatic masking of sensitive values in logs
@@ -99,6 +101,25 @@ interface ConfigLoaderOptions<T> {
99
101
  // Directory with additional YAML files to merge (default: '/etc/app/config')
100
102
  // Set to false to disable
101
103
  overrideDir?: string | false;
104
+
105
+ // Remove empty strings and empty objects from config (default: false)
106
+ // Useful for optional fields with ${VAR:} placeholders
107
+ stripEmpty?: boolean;
108
+
109
+ // HashiCorp Vault options (requires loadConfigAsync)
110
+ vault?: {
111
+ endpoint: string; // Vault server URL
112
+ roleId: string; // AppRole role_id
113
+ secretId: string; // AppRole secret_id
114
+ namespace?: string; // Optional Vault Enterprise namespace
115
+ };
116
+
117
+ // AWS Secrets Manager options (requires loadConfigAsync)
118
+ aws?: {
119
+ region: string; // AWS region
120
+ accessKeyId: string; // AWS access key ID
121
+ secretAccessKey: string; // AWS secret access key
122
+ };
102
123
  }
103
124
  ```
104
125
 
@@ -113,6 +134,160 @@ database:
113
134
  password: ${DB_PASSWORD} # Required - throws if not set
114
135
  ```
115
136
 
137
+ ## Optional Fields
138
+
139
+ Use `${VAR:}` (empty default) with `stripEmpty: true` to support optional fields:
140
+
141
+ ```yaml
142
+ # config/base.yml
143
+ server:
144
+ port: 3000
145
+
146
+ monitoring:
147
+ serviceId: ${MONITORING_SERVICE_ID:}
148
+ apiKey: ${MONITORING_API_KEY:}
149
+ healthcheckUrls:
150
+ cleanup: ${MONITORING_CLEANUP_URL:}
151
+ sync: ${MONITORING_SYNC_URL:}
152
+ ```
153
+
154
+ ```typescript
155
+ const schema = z.object({
156
+ server: z.object({ port: z.number() }),
157
+ monitoring: z.object({
158
+ serviceId: z.string(),
159
+ apiKey: z.string(),
160
+ healthcheckUrls: z.object({
161
+ cleanup: z.string().url().optional(),
162
+ sync: z.string().url().optional(),
163
+ }).optional(),
164
+ }).optional(),
165
+ });
166
+
167
+ const { config } = loadConfig({
168
+ schema,
169
+ stripEmpty: true, // Empty strings → undefined, empty objects removed
170
+ });
171
+
172
+ // If no MONITORING_* env vars are set, config.monitoring will be undefined
173
+ ```
174
+
175
+ **How `stripEmpty` works:**
176
+ - `""` (empty string) → `undefined`
177
+ - Objects with all `undefined` values → removed
178
+ - Arrays: empty strings filtered out
179
+ - Other falsy values (`0`, `false`, `null`) are preserved
180
+
181
+ ## HashiCorp Vault Integration
182
+
183
+ Read secrets directly from HashiCorp Vault using AppRole authentication.
184
+
185
+ ### Setup
186
+
187
+ ```yaml
188
+ # config/base.yml
189
+ database:
190
+ host: localhost
191
+ password: ${vault:secret/data/api:DB_PASSWORD}
192
+ port: ${vault:secret/data/api:DB_PORT:5432} # with default value
193
+ ```
194
+
195
+ ```typescript
196
+ import { loadConfigAsync } from '@rawnodes/config-loader';
197
+
198
+ const { config } = await loadConfigAsync({
199
+ configDir: './config',
200
+ vault: {
201
+ endpoint: 'https://vault.example.com',
202
+ roleId: process.env.VAULT_ROLE_ID!,
203
+ secretId: process.env.VAULT_SECRET_ID!,
204
+ namespace: 'optional-namespace', // for Vault Enterprise
205
+ },
206
+ });
207
+ ```
208
+
209
+ ### Syntax
210
+
211
+ ```
212
+ ${vault:PATH:FIELD} - Required secret
213
+ ${vault:PATH:FIELD:DEFAULT} - With default value (used if secret not found)
214
+ ```
215
+
216
+ **Examples:**
217
+ ```yaml
218
+ database:
219
+ # Read DB_PASSWORD from secret/data/api
220
+ password: ${vault:secret/data/api:DB_PASSWORD}
221
+
222
+ # With default value (supports colons in default)
223
+ url: ${vault:secret/data/db:URL:postgres://localhost:5432/app}
224
+
225
+ # Mix with env variables
226
+ host: ${DB_HOST:localhost}
227
+ ```
228
+
229
+ ### Notes
230
+
231
+ - Use `loadConfigAsync()` instead of `loadConfig()` when using Vault
232
+ - Secrets are cached per path during config load (multiple fields from same path = 1 API call)
233
+ - Supports Vault KV v1 and v2 secret engines
234
+
235
+ ## AWS Secrets Manager Integration
236
+
237
+ Read secrets from AWS Secrets Manager.
238
+
239
+ ### Setup
240
+
241
+ ```yaml
242
+ # config/base.yml
243
+ database:
244
+ password: ${aws:my-app/database:DB_PASSWORD}
245
+ host: ${aws:my-app/database:DB_HOST:localhost}
246
+ api:
247
+ key: ${aws:my-app/api-key} # plain string secret (no field)
248
+ ```
249
+
250
+ ```typescript
251
+ import { loadConfigAsync } from '@rawnodes/config-loader';
252
+
253
+ const { config } = await loadConfigAsync({
254
+ configDir: './config',
255
+ aws: {
256
+ region: 'us-east-1',
257
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
258
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
259
+ },
260
+ });
261
+ ```
262
+
263
+ ### Syntax
264
+
265
+ ```
266
+ ${aws:SECRET_NAME} - Plain string secret (entire value)
267
+ ${aws:SECRET_NAME:FIELD} - JSON secret, extract field
268
+ ${aws:SECRET_NAME:FIELD:DEFAULT} - With default value
269
+ ```
270
+
271
+ **Examples:**
272
+ ```yaml
273
+ database:
274
+ # JSON secret: {"DB_PASSWORD": "secret", "DB_USER": "admin"}
275
+ password: ${aws:my-app/database:DB_PASSWORD}
276
+ user: ${aws:my-app/database:DB_USER}
277
+
278
+ # Plain string secret (no field)
279
+ api_key: ${aws:my-app/api-key}
280
+
281
+ # With default value
282
+ host: ${aws:my-app/database:DB_HOST:localhost}
283
+ ```
284
+
285
+ ### Notes
286
+
287
+ - Use `loadConfigAsync()` instead of `loadConfig()` when using AWS
288
+ - Secrets are cached per secret name during config load
289
+ - Supports both JSON and plain string secrets
290
+
116
291
  ## Zod Validation
117
292
 
118
293
  ```typescript
@@ -186,7 +361,7 @@ export class ConfigModule {}
186
361
 
187
362
  ### `loadConfig<T>(options?): ConfigLoaderResult<T>`
188
363
 
189
- Loads and merges configuration files.
364
+ Loads and merges configuration files synchronously.
190
365
 
191
366
  **Returns:**
192
367
  ```typescript
@@ -197,6 +372,21 @@ interface ConfigLoaderResult<T> {
197
372
  }
198
373
  ```
199
374
 
375
+ ### `loadConfigAsync<T>(options?): Promise<ConfigLoaderResult<T>>`
376
+
377
+ Async version with Vault support. Required when using `vault` option.
378
+
379
+ ```typescript
380
+ const { config } = await loadConfigAsync({
381
+ configDir: './config',
382
+ vault: {
383
+ endpoint: 'https://vault.example.com',
384
+ roleId: 'role-id',
385
+ secretId: 'secret-id',
386
+ },
387
+ });
388
+ ```
389
+
200
390
  ### `maskSecrets(obj): unknown`
201
391
 
202
392
  Masks sensitive values in an object. Useful for logging.
@@ -220,6 +410,20 @@ Deep merges two objects.
220
410
 
221
411
  Replaces `${VAR}` placeholders with environment variable values.
222
412
 
413
+ ### `stripEmpty(obj): unknown`
414
+
415
+ Removes empty strings and empty objects recursively. Useful for cleaning up config before validation.
416
+
417
+ ```typescript
418
+ import { stripEmpty } from '@rawnodes/config-loader';
419
+
420
+ const cleaned = stripEmpty({
421
+ server: { port: 3000 },
422
+ optional: { url: '', name: '' },
423
+ });
424
+ // { server: { port: 3000 } }
425
+ ```
426
+
223
427
  ## License
224
428
 
225
429
  MIT
package/dist/index.d.mts CHANGED
@@ -1,5 +1,39 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ interface VaultOptions {
4
+ /**
5
+ * Vault server endpoint URL
6
+ * @example 'https://vault.example.com'
7
+ */
8
+ endpoint: string;
9
+ /**
10
+ * AppRole role_id for authentication
11
+ */
12
+ roleId: string;
13
+ /**
14
+ * AppRole secret_id for authentication
15
+ */
16
+ secretId: string;
17
+ /**
18
+ * Optional namespace for Vault Enterprise
19
+ */
20
+ namespace?: string;
21
+ }
22
+ interface AwsSecretsOptions {
23
+ /**
24
+ * AWS region
25
+ * @example 'us-east-1'
26
+ */
27
+ region: string;
28
+ /**
29
+ * AWS access key ID
30
+ */
31
+ accessKeyId: string;
32
+ /**
33
+ * AWS secret access key
34
+ */
35
+ secretAccessKey: string;
36
+ }
3
37
  interface DotenvOptions {
4
38
  /**
5
39
  * Path to .env file
@@ -55,6 +89,26 @@ interface ConfigLoaderOptions<T = unknown> {
55
89
  * @default '/etc/app/config'
56
90
  */
57
91
  overrideDir?: string | false;
92
+ /**
93
+ * Remove empty strings and empty objects from config
94
+ * Useful for optional fields with ${VAR:} placeholders
95
+ * - Empty strings "" become undefined
96
+ * - Objects with all undefined values are removed
97
+ * @default false
98
+ */
99
+ stripEmpty?: boolean;
100
+ /**
101
+ * HashiCorp Vault options for secret retrieval
102
+ * If provided, ${vault:path:field} placeholders will be resolved
103
+ * Requires using loadConfigAsync() instead of loadConfig()
104
+ */
105
+ vault?: VaultOptions;
106
+ /**
107
+ * AWS Secrets Manager options for secret retrieval
108
+ * If provided, ${aws:secret-name:field} placeholders will be resolved
109
+ * Requires using loadConfigAsync() instead of loadConfig()
110
+ */
111
+ aws?: AwsSecretsOptions;
58
112
  }
59
113
  interface ConfigLoaderResult<T> {
60
114
  config: T;
@@ -63,12 +117,37 @@ interface ConfigLoaderResult<T> {
63
117
  }
64
118
 
65
119
  declare function loadConfig<T>(options?: ConfigLoaderOptions<T>): ConfigLoaderResult<T>;
120
+ declare function loadConfigAsync<T>(options?: ConfigLoaderOptions<T>): Promise<ConfigLoaderResult<T>>;
66
121
 
67
122
  type DeepObject = Record<string, unknown>;
68
123
  declare function deepMerge(base: DeepObject, override: DeepObject): DeepObject;
69
124
 
70
- declare function replacePlaceholders(obj: unknown): unknown;
125
+ interface VaultSecretCache {
126
+ [path: string]: Record<string, unknown>;
127
+ }
128
+ interface VaultClient {
129
+ fetchSecrets(obj: unknown): Promise<VaultSecretCache>;
130
+ getSecret(path: string, field: string): unknown;
131
+ }
132
+ declare function createVaultClient(options: VaultOptions): Promise<VaultClient>;
133
+
134
+ interface AwsSecretsCache {
135
+ [secretName: string]: string | Record<string, unknown>;
136
+ }
137
+ interface AwsSecretsClient {
138
+ fetchSecrets(obj: unknown): Promise<AwsSecretsCache>;
139
+ getSecret(secretName: string, field?: string): unknown;
140
+ }
141
+ declare function createAwsSecretsClient(options: AwsSecretsOptions): Promise<AwsSecretsClient>;
142
+
143
+ interface ReplacePlaceholdersOptions {
144
+ vaultClient?: VaultClient;
145
+ awsClient?: AwsSecretsClient;
146
+ }
147
+ declare function replacePlaceholders(obj: unknown, options?: ReplacePlaceholdersOptions): unknown;
71
148
 
72
149
  declare function maskSecrets(obj: unknown): unknown;
73
150
 
74
- export { type ConfigLoaderOptions, type ConfigLoaderResult, type DotenvOptions, deepMerge, loadConfig, maskSecrets, replacePlaceholders };
151
+ declare function stripEmpty(obj: unknown): unknown;
152
+
153
+ export { type AwsSecretsCache, type AwsSecretsClient, type AwsSecretsOptions, type ConfigLoaderOptions, type ConfigLoaderResult, type DotenvOptions, type VaultClient, type VaultOptions, type VaultSecretCache, createAwsSecretsClient, createVaultClient, deepMerge, loadConfig, loadConfigAsync, maskSecrets, replacePlaceholders, stripEmpty };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,39 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ interface VaultOptions {
4
+ /**
5
+ * Vault server endpoint URL
6
+ * @example 'https://vault.example.com'
7
+ */
8
+ endpoint: string;
9
+ /**
10
+ * AppRole role_id for authentication
11
+ */
12
+ roleId: string;
13
+ /**
14
+ * AppRole secret_id for authentication
15
+ */
16
+ secretId: string;
17
+ /**
18
+ * Optional namespace for Vault Enterprise
19
+ */
20
+ namespace?: string;
21
+ }
22
+ interface AwsSecretsOptions {
23
+ /**
24
+ * AWS region
25
+ * @example 'us-east-1'
26
+ */
27
+ region: string;
28
+ /**
29
+ * AWS access key ID
30
+ */
31
+ accessKeyId: string;
32
+ /**
33
+ * AWS secret access key
34
+ */
35
+ secretAccessKey: string;
36
+ }
3
37
  interface DotenvOptions {
4
38
  /**
5
39
  * Path to .env file
@@ -55,6 +89,26 @@ interface ConfigLoaderOptions<T = unknown> {
55
89
  * @default '/etc/app/config'
56
90
  */
57
91
  overrideDir?: string | false;
92
+ /**
93
+ * Remove empty strings and empty objects from config
94
+ * Useful for optional fields with ${VAR:} placeholders
95
+ * - Empty strings "" become undefined
96
+ * - Objects with all undefined values are removed
97
+ * @default false
98
+ */
99
+ stripEmpty?: boolean;
100
+ /**
101
+ * HashiCorp Vault options for secret retrieval
102
+ * If provided, ${vault:path:field} placeholders will be resolved
103
+ * Requires using loadConfigAsync() instead of loadConfig()
104
+ */
105
+ vault?: VaultOptions;
106
+ /**
107
+ * AWS Secrets Manager options for secret retrieval
108
+ * If provided, ${aws:secret-name:field} placeholders will be resolved
109
+ * Requires using loadConfigAsync() instead of loadConfig()
110
+ */
111
+ aws?: AwsSecretsOptions;
58
112
  }
59
113
  interface ConfigLoaderResult<T> {
60
114
  config: T;
@@ -63,12 +117,37 @@ interface ConfigLoaderResult<T> {
63
117
  }
64
118
 
65
119
  declare function loadConfig<T>(options?: ConfigLoaderOptions<T>): ConfigLoaderResult<T>;
120
+ declare function loadConfigAsync<T>(options?: ConfigLoaderOptions<T>): Promise<ConfigLoaderResult<T>>;
66
121
 
67
122
  type DeepObject = Record<string, unknown>;
68
123
  declare function deepMerge(base: DeepObject, override: DeepObject): DeepObject;
69
124
 
70
- declare function replacePlaceholders(obj: unknown): unknown;
125
+ interface VaultSecretCache {
126
+ [path: string]: Record<string, unknown>;
127
+ }
128
+ interface VaultClient {
129
+ fetchSecrets(obj: unknown): Promise<VaultSecretCache>;
130
+ getSecret(path: string, field: string): unknown;
131
+ }
132
+ declare function createVaultClient(options: VaultOptions): Promise<VaultClient>;
133
+
134
+ interface AwsSecretsCache {
135
+ [secretName: string]: string | Record<string, unknown>;
136
+ }
137
+ interface AwsSecretsClient {
138
+ fetchSecrets(obj: unknown): Promise<AwsSecretsCache>;
139
+ getSecret(secretName: string, field?: string): unknown;
140
+ }
141
+ declare function createAwsSecretsClient(options: AwsSecretsOptions): Promise<AwsSecretsClient>;
142
+
143
+ interface ReplacePlaceholdersOptions {
144
+ vaultClient?: VaultClient;
145
+ awsClient?: AwsSecretsClient;
146
+ }
147
+ declare function replacePlaceholders(obj: unknown, options?: ReplacePlaceholdersOptions): unknown;
71
148
 
72
149
  declare function maskSecrets(obj: unknown): unknown;
73
150
 
74
- export { type ConfigLoaderOptions, type ConfigLoaderResult, type DotenvOptions, deepMerge, loadConfig, maskSecrets, replacePlaceholders };
151
+ declare function stripEmpty(obj: unknown): unknown;
152
+
153
+ export { type AwsSecretsCache, type AwsSecretsClient, type AwsSecretsOptions, type ConfigLoaderOptions, type ConfigLoaderResult, type DotenvOptions, type VaultClient, type VaultOptions, type VaultSecretCache, createAwsSecretsClient, createVaultClient, deepMerge, loadConfig, loadConfigAsync, maskSecrets, replacePlaceholders, stripEmpty };