@geekmidas/envkit 9.0.2 → 10.0.0-alpha.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.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/dist/{EnvironmentBuilder-DDgLJAAo.cjs → EnvironmentBuilder-15SdFJdK.cjs} +2 -2
  3. package/dist/EnvironmentBuilder-15SdFJdK.cjs.map +1 -0
  4. package/dist/{EnvironmentBuilder-CFen3oIg.mjs → EnvironmentBuilder-C-2fViCT.mjs} +2 -2
  5. package/dist/EnvironmentBuilder-C-2fViCT.mjs.map +1 -0
  6. package/dist/{EnvironmentBuilder-DHfDXJUm.d.mts → EnvironmentBuilder-CoBQ9Xp2.d.mts} +2 -2
  7. package/dist/{EnvironmentBuilder-CNgcdzSR.d.cts.map → EnvironmentBuilder-CoBQ9Xp2.d.mts.map} +1 -1
  8. package/dist/{EnvironmentBuilder-CNgcdzSR.d.cts → EnvironmentBuilder-DeIle4QN.d.cts} +2 -2
  9. package/dist/{EnvironmentBuilder-DHfDXJUm.d.mts.map → EnvironmentBuilder-DeIle4QN.d.cts.map} +1 -1
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.d.cts +1 -1
  12. package/dist/index.d.mts +1 -1
  13. package/dist/index.mjs +1 -1
  14. package/dist/sst.cjs +13 -6
  15. package/dist/sst.cjs.map +1 -1
  16. package/dist/sst.d.cts +15 -4
  17. package/dist/sst.d.cts.map +1 -1
  18. package/dist/sst.d.mts +15 -4
  19. package/dist/sst.d.mts.map +1 -1
  20. package/dist/sst.mjs +13 -6
  21. package/dist/sst.mjs.map +1 -1
  22. package/package.json +6 -2
  23. package/CHANGELOG.md +0 -115
  24. package/dist/EnvironmentBuilder-CFen3oIg.mjs.map +0 -1
  25. package/dist/EnvironmentBuilder-DDgLJAAo.cjs.map +0 -1
  26. package/docs/api-reference.md +0 -302
  27. package/docs/async-secrets-design.md +0 -355
  28. package/examples/basic-usage.ts +0 -386
  29. package/src/EnvironmentBuilder.ts +0 -192
  30. package/src/EnvironmentParser.ts +0 -330
  31. package/src/SnifferEnvironmentParser.ts +0 -334
  32. package/src/SstEnvValidator.ts +0 -369
  33. package/src/SstEnvironmentBuilder.ts +0 -343
  34. package/src/__tests__/ConfigParser.spec.ts +0 -394
  35. package/src/__tests__/EnvironmentBuilder.spec.ts +0 -254
  36. package/src/__tests__/EnvironmentParser.spec.ts +0 -839
  37. package/src/__tests__/SnifferEnvironmentParser.spec.ts +0 -644
  38. package/src/__tests__/SstEnvValidator.spec.ts +0 -236
  39. package/src/__tests__/SstEnvironmentBuilder.spec.ts +0 -397
  40. package/src/__tests__/credentials.integration.spec.ts +0 -239
  41. package/src/__tests__/credentials.spec.ts +0 -136
  42. package/src/__tests__/formatter.spec.ts +0 -268
  43. package/src/__tests__/sst.spec.ts +0 -437
  44. package/src/credentials.ts +0 -112
  45. package/src/formatter.ts +0 -146
  46. package/src/index.ts +0 -24
  47. package/src/sst.ts +0 -76
  48. package/sst-env.d.ts +0 -8
  49. package/tsconfig.json +0 -9
  50. package/tsdown.config.ts +0 -18
@@ -1,302 +0,0 @@
1
- # API Reference - @geekmidas/envkit
2
-
3
- ## Classes
4
-
5
- ### `EnvironmentParser`
6
-
7
- The main class for creating configuration parsers.
8
-
9
- ```typescript
10
- class EnvironmentParser {
11
- constructor(config: Record<string, unknown>)
12
- create<T>(schemaBuilder: (get: GetFunction) => T): ConfigParser<T>
13
- }
14
- ```
15
-
16
- #### Constructor
17
-
18
- Creates a new environment parser instance.
19
-
20
- **Parameters:**
21
- - `config: Record<string, unknown>` - The configuration object to parse (typically `process.env`)
22
-
23
- **Example:**
24
- ```typescript
25
- const parser = new EnvironmentParser(process.env);
26
- ```
27
-
28
- #### Methods
29
-
30
- ##### `create<T>(schemaBuilder: (get: GetFunction) => T): ConfigParser<T>`
31
-
32
- Creates a configuration parser with the specified schema.
33
-
34
- **Parameters:**
35
- - `schemaBuilder: (get: GetFunction) => T` - A function that receives a `get` function and returns the schema definition
36
-
37
- **Returns:**
38
- - `ConfigParser<T>` - A configuration parser instance
39
-
40
- **Example:**
41
- ```typescript
42
- const config = parser.create((get) => ({
43
- port: get('PORT').string().transform(Number),
44
- apiKey: get('API_KEY').string()
45
- }));
46
- ```
47
-
48
- ### `ConfigParser<T>`
49
-
50
- The configuration parser returned by `EnvironmentParser.create()`.
51
-
52
- ```typescript
53
- class ConfigParser<T> {
54
- parse(): T
55
- }
56
- ```
57
-
58
- #### Methods
59
-
60
- ##### `parse(): T`
61
-
62
- Parses and validates the configuration according to the defined schema.
63
-
64
- **Returns:**
65
- - `T` - The parsed and validated configuration object
66
-
67
- **Throws:**
68
- - `ZodError` - If validation fails. The error contains all validation failures aggregated together.
69
-
70
- **Example:**
71
- ```typescript
72
- try {
73
- const config = parser.create((get) => ({
74
- port: get('PORT').string().transform(Number)
75
- })).parse();
76
-
77
- console.log(config.port); // number
78
- } catch (error) {
79
- if (error instanceof z.ZodError) {
80
- console.error('Validation errors:', error.errors);
81
- }
82
- }
83
- ```
84
-
85
- ## Type Definitions
86
-
87
- ### `GetFunction`
88
-
89
- The function passed to the schema builder for accessing configuration values.
90
-
91
- ```typescript
92
- type GetFunction = (path: string) => ZodTypeAny
93
- ```
94
-
95
- **Parameters:**
96
- - `path: string` - The path to the configuration value. Supports:
97
- - Simple paths: `'PORT'`, `'API_KEY'`
98
- - Nested paths with dots: `'database.host'`, `'api.endpoints.users'`
99
-
100
- **Returns:**
101
- - `ZodTypeAny` - A Zod schema that will be used to validate the value at the specified path
102
-
103
- **Usage:**
104
- ```typescript
105
- const config = parser.create((get) => ({
106
- // Simple path
107
- port: get('PORT').string(),
108
-
109
- // Nested path - looks for 'DATABASE_HOST' in config
110
- database: {
111
- host: get('DATABASE_HOST').string()
112
- },
113
-
114
- // With transformations
115
- maxRetries: get('MAX_RETRIES').string().transform(Number),
116
-
117
- // With validation
118
- email: get('ADMIN_EMAIL').string().email(),
119
-
120
- // With defaults
121
- logLevel: get('LOG_LEVEL').string().default('info'),
122
-
123
- // Optional values
124
- debugMode: get('DEBUG').string().optional()
125
- }));
126
- ```
127
-
128
- ## Error Handling
129
-
130
- ### Validation Errors
131
-
132
- When validation fails, the parser throws a `ZodError` containing all validation failures:
133
-
134
- ```typescript
135
- import { z } from 'zod';
136
-
137
- try {
138
- const config = parser.create((get) => ({
139
- port: get('PORT').string().transform(Number),
140
- apiKey: get('API_KEY').string().min(32),
141
- email: get('ADMIN_EMAIL').string().email()
142
- })).parse();
143
- } catch (error) {
144
- if (error instanceof z.ZodError) {
145
- // error.errors is an array of all validation errors
146
- error.errors.forEach(err => {
147
- console.error(`${err.path.join('.')}: ${err.message}`);
148
- });
149
-
150
- // Example output:
151
- // port: Expected string, received undefined
152
- // apiKey: String must contain at least 32 character(s)
153
- // email: Invalid email
154
- }
155
- }
156
- ```
157
-
158
- ### Error Aggregation
159
-
160
- The parser collects all validation errors before throwing, allowing you to see all configuration problems at once:
161
-
162
- ```typescript
163
- // If multiple values are invalid, all errors are reported together
164
- const config = parser.create((get) => ({
165
- database: {
166
- host: get('DB_HOST').string(), // Missing
167
- port: get('DB_PORT').string(), // Missing
168
- name: get('DB_NAME').string() // Missing
169
- },
170
- api: {
171
- key: get('API_KEY').string(), // Missing
172
- url: get('API_URL').url() // Invalid format
173
- }
174
- }));
175
-
176
- // Throws ZodError with all 5 validation errors
177
- ```
178
-
179
- ## Path Resolution
180
-
181
- The parser uses lodash's `get` and `set` functions for path resolution:
182
-
183
- ### Simple Paths
184
- ```typescript
185
- // Configuration object
186
- const config = {
187
- PORT: '3000',
188
- API_KEY: 'secret'
189
- };
190
-
191
- // Access with get()
192
- get('PORT') // Looks for config.PORT
193
- get('API_KEY') // Looks for config.API_KEY
194
- ```
195
-
196
- ### Nested Objects
197
- ```typescript
198
- // Configuration object
199
- const config = {
200
- database: {
201
- host: 'localhost',
202
- port: '5432'
203
- }
204
- };
205
-
206
- // Access with get()
207
- get('database.host') // Returns 'localhost'
208
- get('database.port') // Returns '5432'
209
- ```
210
-
211
- ### Environment Variable Mapping
212
- When using `process.env`, nested paths are automatically mapped:
213
-
214
- ```typescript
215
- // These environment variables:
216
- // DATABASE_HOST=localhost
217
- // DATABASE_PORT=5432
218
-
219
- // Can be accessed as:
220
- const config = parser.create((get) => ({
221
- database: {
222
- host: get('DATABASE_HOST').string(), // Maps to DATABASE_HOST
223
- port: get('DATABASE_PORT').string() // Maps to DATABASE_PORT
224
- }
225
- }));
226
- ```
227
-
228
- ## Zod Schema Integration
229
-
230
- The parser returns Zod schemas, allowing you to use all Zod validation features:
231
-
232
- ### Transformations
233
- ```typescript
234
- get('PORT').string().transform(Number)
235
- get('ENABLED').string().transform(v => v === 'true')
236
- get('TAGS').string().transform(v => v.split(','))
237
- ```
238
-
239
- ### Validations
240
- ```typescript
241
- get('EMAIL').string().email()
242
- get('URL').string().url()
243
- get('PORT').string().transform(Number).int().min(1).max(65535)
244
- get('NODE_ENV').enum(['development', 'production', 'test'])
245
- ```
246
-
247
- ### Refinements
248
- ```typescript
249
- get('PASSWORD')
250
- .string()
251
- .min(8)
252
- .refine(password => /[A-Z]/.test(password), {
253
- message: 'Password must contain at least one uppercase letter'
254
- })
255
- ```
256
-
257
- ### Complex Types
258
- ```typescript
259
- // Arrays
260
- get('ALLOWED_ORIGINS')
261
- .string()
262
- .transform(v => v.split(','))
263
- .pipe(z.array(z.string().url()))
264
-
265
- // Objects
266
- get('CONFIG_JSON')
267
- .string()
268
- .transform(v => JSON.parse(v))
269
- .pipe(z.object({
270
- timeout: z.number(),
271
- retries: z.number()
272
- }))
273
- ```
274
-
275
- ## TypeScript Integration
276
-
277
- The parser provides full type inference:
278
-
279
- ```typescript
280
- const parser = new EnvironmentParser(process.env);
281
-
282
- const config = parser.create((get) => ({
283
- server: {
284
- port: get('PORT').string().transform(Number),
285
- host: get('HOST').string().default('localhost')
286
- },
287
- features: {
288
- auth: get('FEATURE_AUTH').string().transform(v => v === 'true'),
289
- rateLimit: get('FEATURE_RATE_LIMIT').string().transform(v => v === 'true')
290
- }
291
- }));
292
-
293
- const parsed = config.parse();
294
-
295
- // TypeScript knows:
296
- // parsed.server.port: number
297
- // parsed.server.host: string
298
- // parsed.features.auth: boolean
299
- // parsed.features.rateLimit: boolean
300
- ```
301
-
302
- The types are automatically inferred from the Zod schemas, providing complete type safety without manual type definitions.
@@ -1,355 +0,0 @@
1
- # Async Secrets Resolution Design
2
-
3
- ## Problem Statement
4
-
5
- Applications often need to fetch sensitive configuration values (secrets) from external providers like HashiCorp Vault, AWS Secrets Manager, or other secret management systems. The current `EnvironmentParser` only supports synchronous parsing of environment variables, which doesn't accommodate async secret fetching.
6
-
7
- Additionally, environment variables for secrets typically contain **references** to the actual secret (e.g., a Vault path), not the secret value itself:
8
-
9
- ```bash
10
- # Environment variables
11
- DB_HOST=localhost # Actual value
12
- DB_PASSWORD=/vault/prod/db # Reference to secret, not the actual password
13
- API_KEY=/vault/prod/api # Reference to secret
14
- ```
15
-
16
- ## Proposed Solution
17
-
18
- Extend `EnvironmentParser` with:
19
- 1. A separate `getSecret()` getter to distinguish secrets from regular env vars
20
- 2. A configurable `secretsResolver` that fetches actual values from refs
21
- 3. A cache to avoid redundant fetches for the same ref
22
- 4. An `echoSecretsResolver` for testing that returns refs as values
23
-
24
- ## API Design
25
-
26
- ### Constructor Options
27
-
28
- ```typescript
29
- interface EnvironmentParserOptions {
30
- /**
31
- * Function to resolve secret references to actual values.
32
- * Receives an array of refs (values from env vars marked as secrets).
33
- * Returns a Map of ref → resolved value.
34
- */
35
- secretsResolver?: SecretsResolver;
36
- }
37
-
38
- type SecretsResolver = (refs: string[]) => Promise<Map<string, string>>;
39
- ```
40
-
41
- ### Getters
42
-
43
- ```typescript
44
- parser.create((get, getSecret) => ({
45
- // Regular env var - resolved synchronously
46
- host: get('DB_HOST').string(),
47
- port: get('PORT').string().transform(Number),
48
-
49
- // Secret env var - resolved asynchronously via secretsResolver
50
- // The env var value is treated as a ref, not the actual value
51
- password: getSecret('DB_PASSWORD').string(),
52
- apiKey: getSecret('API_KEY').string(),
53
- }));
54
- ```
55
-
56
- ### Parsed Config Types
57
-
58
- ```typescript
59
- // Regular values are their actual types
60
- config.host // string
61
- config.port // number
62
-
63
- // Secret values are Promises of their types
64
- config.password // Promise<string>
65
- config.apiKey // Promise<string>
66
-
67
- // Usage
68
- const password = await config.password;
69
- ```
70
-
71
- ### Built-in Resolvers
72
-
73
- ```typescript
74
- import { echoSecretsResolver } from '@geekmidas/envkit';
75
-
76
- /**
77
- * Echo resolver returns the ref as the value.
78
- * Useful for testing where the "ref" IS the actual test value.
79
- */
80
- export const echoSecretsResolver: SecretsResolver = async (refs) =>
81
- new Map(refs.map((ref) => [ref, ref]));
82
- ```
83
-
84
- ## Resolution Flow
85
-
86
- ```
87
- ┌─────────────────────────────────────────────────────────────────────┐
88
- │ parse() called │
89
- └─────────────────────────────────────────────────────────────────────┘
90
- │
91
- ▼
92
- ┌─────────────────────────────────────────────────────────────────────┐
93
- │ 1. Regular env vars (get) are parsed synchronously as normal │
94
- │ config.host = "localhost" │
95
- │ config.port = 3000 │
96
- └─────────────────────────────────────────────────────────────────────┘
97
- │
98
- ▼
99
- ┌─────────────────────────────────────────────────────────────────────┐
100
- │ 2. Secret env vars (getSecret) return Promises │
101
- │ config.password = Promise<string> │
102
- │ config.apiKey = Promise<string> │
103
- └─────────────────────────────────────────────────────────────────────┘
104
- │
105
- ▼
106
- ┌─────────────────────────────────────────────────────────────────────┐
107
- │ 3. When Promise is awaited: │
108
- │ a. Read ref from env var (e.g., "/vault/prod/db") │
109
- │ b. Check cache - if cached, return cached value │
110
- │ c. If not cached, call secretsResolver([ref]) │
111
- │ d. Cache the resolved value │
112
- │ e. Apply Zod validation/transformation │
113
- │ f. Return validated value │
114
- └─────────────────────────────────────────────────────────────────────┘
115
- ```
116
-
117
- ## Caching Strategy
118
-
119
- A resolved secrets cache prevents redundant API calls:
120
-
121
- ```typescript
122
- // Internal cache (per EnvironmentParser instance)
123
- private resolvedCache = new Map<string, string>();
124
-
125
- async resolveSecret(ref: string): Promise<string> {
126
- // Return cached value if available
127
- if (this.resolvedCache.has(ref)) {
128
- return this.resolvedCache.get(ref)!;
129
- }
130
-
131
- // Fetch from resolver
132
- const resolved = await this.secretsResolver([ref]);
133
- const value = resolved.get(ref);
134
-
135
- if (value === undefined) {
136
- throw new Error(`Secret resolver did not return value for ref: ${ref}`);
137
- }
138
-
139
- // Cache for future use
140
- this.resolvedCache.set(ref, value);
141
- return value;
142
- }
143
- ```
144
-
145
- Benefits:
146
- - Same ref accessed multiple times → single resolver call
147
- - Consistent values within a parser instance
148
- - Reduces load on secret providers
149
-
150
- ## Usage Examples
151
-
152
- ### Production with Vault
153
-
154
- ```typescript
155
- import { EnvironmentParser } from '@geekmidas/envkit';
156
-
157
- // Vault resolver implementation
158
- const vaultResolver: SecretsResolver = async (refs) => {
159
- const secrets = await vaultClient.batchRead(refs);
160
- return new Map(refs.map((ref, i) => [ref, secrets[i].value]));
161
- };
162
-
163
- const parser = new EnvironmentParser(process.env, {
164
- secretsResolver: vaultResolver,
165
- });
166
-
167
- const config = parser.create((get, getSecret) => ({
168
- database: {
169
- host: get('DB_HOST').string(),
170
- port: get('DB_PORT').coerce.number(),
171
- password: getSecret('DB_PASSWORD').string(),
172
- },
173
- api: {
174
- baseUrl: get('API_BASE_URL').string().url(),
175
- key: getSecret('API_KEY').string().min(32),
176
- },
177
- })).parse();
178
-
179
- // Use in service registration
180
- const databaseService = {
181
- serviceName: 'database' as const,
182
- async register(envParser: EnvironmentParser<{}>) {
183
- const config = envParser.create((get, getSecret) => ({
184
- host: get('DB_HOST').string(),
185
- password: getSecret('DB_PASSWORD').string(),
186
- })).parse();
187
-
188
- // Await the secret
189
- const password = await config.password;
190
-
191
- return new Database({
192
- host: config.host,
193
- password,
194
- });
195
- },
196
- };
197
- ```
198
-
199
- ### Testing with Echo Resolver
200
-
201
- ```typescript
202
- import { EnvironmentParser, echoSecretsResolver } from '@geekmidas/envkit';
203
-
204
- describe('DatabaseService', () => {
205
- it('should connect with credentials', async () => {
206
- // In tests, the "ref" IS the actual test value
207
- const env = {
208
- DB_HOST: 'localhost',
209
- DB_PORT: '5432',
210
- DB_PASSWORD: 'test-password-123', // This IS the password for tests
211
- };
212
-
213
- const parser = new EnvironmentParser(env, {
214
- secretsResolver: echoSecretsResolver,
215
- });
216
-
217
- const service = await databaseService.register(parser);
218
-
219
- expect(service.isConnected()).toBe(true);
220
- });
221
- });
222
- ```
223
-
224
- ### AWS Secrets Manager
225
-
226
- ```typescript
227
- import { SecretsManagerClient, BatchGetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
228
-
229
- const awsResolver: SecretsResolver = async (refs) => {
230
- const client = new SecretsManagerClient({});
231
- const command = new BatchGetSecretValueCommand({
232
- SecretIdList: refs,
233
- });
234
-
235
- const response = await client.send(command);
236
- const result = new Map<string, string>();
237
-
238
- for (const secret of response.SecretValues ?? []) {
239
- if (secret.ARN && secret.SecretString) {
240
- result.set(secret.ARN, secret.SecretString);
241
- }
242
- }
243
-
244
- return result;
245
- };
246
- ```
247
-
248
- ## Type Definitions
249
-
250
- ```typescript
251
- /**
252
- * Function type for resolving secret references to actual values.
253
- */
254
- export type SecretsResolver = (refs: string[]) => Promise<Map<string, string>>;
255
-
256
- /**
257
- * Extended getter that includes secret() method.
258
- */
259
- export type SecretEnvFetcher<TPath extends string = string> = (
260
- name: TPath,
261
- ) => typeof z;
262
-
263
- /**
264
- * Builder function signature with both getters.
265
- */
266
- export type EnvironmentBuilderWithSecrets<TResponse extends EmptyObject> = (
267
- get: EnvFetcher,
268
- getSecret: SecretEnvFetcher,
269
- ) => TResponse;
270
-
271
- /**
272
- * Infers config type, wrapping secret values in Promise.
273
- */
274
- export type InferConfigWithSecrets<T extends EmptyObject> = {
275
- [K in keyof T]: T[K] extends SecretSchema<infer U>
276
- ? Promise<U>
277
- : T[K] extends z.ZodSchema
278
- ? z.infer<T[K]>
279
- : T[K] extends Record<string, unknown>
280
- ? InferConfigWithSecrets<T[K]>
281
- : T[K];
282
- };
283
- ```
284
-
285
- ## Error Handling
286
-
287
- ### Missing Resolver
288
-
289
- ```typescript
290
- // If getSecret() is used but no resolver provided
291
- const parser = new EnvironmentParser(env); // no resolver
292
-
293
- const config = parser.create((get, getSecret) => ({
294
- password: getSecret('DB_PASSWORD').string(),
295
- })).parse();
296
-
297
- await config.password;
298
- // Error: SecretsResolver is required when using getSecret().
299
- // Configure it via EnvironmentParser options.
300
- ```
301
-
302
- ### Missing Ref in Environment
303
-
304
- ```typescript
305
- // If env var doesn't exist
306
- const env = { DB_HOST: 'localhost' }; // DB_PASSWORD not set
307
-
308
- const config = parser.create((get, getSecret) => ({
309
- password: getSecret('DB_PASSWORD').string(),
310
- })).parse();
311
-
312
- await config.password;
313
- // Error: Environment variable "DB_PASSWORD" is not defined.
314
- // Expected a secret reference.
315
- ```
316
-
317
- ### Resolver Doesn't Return Value
318
-
319
- ```typescript
320
- // If resolver doesn't return value for a ref
321
- const brokenResolver: SecretsResolver = async (refs) => new Map();
322
-
323
- await config.password;
324
- // Error: Secret resolver did not return value for ref: /vault/prod/db
325
- ```
326
-
327
- ## Migration Path
328
-
329
- Existing code using `EnvironmentParser` continues to work unchanged:
330
-
331
- ```typescript
332
- // Before (still works)
333
- const config = parser.create((get) => ({
334
- port: get('PORT').string().transform(Number),
335
- })).parse();
336
-
337
- // After (opt-in to secrets)
338
- const config = parser.create((get, getSecret) => ({
339
- port: get('PORT').string().transform(Number),
340
- password: getSecret('DB_PASSWORD').string(),
341
- })).parse();
342
- ```
343
-
344
- ## Open Questions
345
-
346
- 1. **Batch resolution timing**: Should we batch all secret resolutions when `parse()` is called, or resolve lazily when each Promise is awaited?
347
- - **Lazy (proposed)**: Each secret resolved on first await, cached for subsequent access
348
- - **Eager**: All secrets resolved upfront in parse(), requires parseAsync()
349
-
350
- 2. **Cache scope**: Should the cache be per-parser instance or global?
351
- - **Per-instance (proposed)**: Isolated, predictable behavior
352
- - **Global**: More efficient for multiple parsers with same refs
353
-
354
- 3. **Cache invalidation**: Should there be a way to clear the cache?
355
- - Could add `parser.clearSecretCache()` method if needed