@geekmidas/envkit 9.0.2 → 10.0.0-alpha.1
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 +2 -2
- package/dist/{EnvironmentBuilder-DDgLJAAo.cjs → EnvironmentBuilder-15SdFJdK.cjs} +2 -2
- package/dist/EnvironmentBuilder-15SdFJdK.cjs.map +1 -0
- package/dist/{EnvironmentBuilder-CFen3oIg.mjs → EnvironmentBuilder-C-2fViCT.mjs} +2 -2
- package/dist/EnvironmentBuilder-C-2fViCT.mjs.map +1 -0
- package/dist/{EnvironmentBuilder-DHfDXJUm.d.mts → EnvironmentBuilder-CoBQ9Xp2.d.mts} +2 -2
- package/dist/{EnvironmentBuilder-CNgcdzSR.d.cts.map → EnvironmentBuilder-CoBQ9Xp2.d.mts.map} +1 -1
- package/dist/{EnvironmentBuilder-CNgcdzSR.d.cts → EnvironmentBuilder-DeIle4QN.d.cts} +2 -2
- package/dist/{EnvironmentBuilder-DHfDXJUm.d.mts.map → EnvironmentBuilder-DeIle4QN.d.cts.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/sst.cjs +13 -6
- package/dist/sst.cjs.map +1 -1
- package/dist/sst.d.cts +15 -4
- package/dist/sst.d.cts.map +1 -1
- package/dist/sst.d.mts +15 -4
- package/dist/sst.d.mts.map +1 -1
- package/dist/sst.mjs +13 -6
- package/dist/sst.mjs.map +1 -1
- package/package.json +6 -2
- package/CHANGELOG.md +0 -115
- package/dist/EnvironmentBuilder-CFen3oIg.mjs.map +0 -1
- package/dist/EnvironmentBuilder-DDgLJAAo.cjs.map +0 -1
- package/docs/api-reference.md +0 -302
- package/docs/async-secrets-design.md +0 -355
- package/examples/basic-usage.ts +0 -386
- package/src/EnvironmentBuilder.ts +0 -192
- package/src/EnvironmentParser.ts +0 -330
- package/src/SnifferEnvironmentParser.ts +0 -334
- package/src/SstEnvValidator.ts +0 -369
- package/src/SstEnvironmentBuilder.ts +0 -343
- package/src/__tests__/ConfigParser.spec.ts +0 -394
- package/src/__tests__/EnvironmentBuilder.spec.ts +0 -254
- package/src/__tests__/EnvironmentParser.spec.ts +0 -839
- package/src/__tests__/SnifferEnvironmentParser.spec.ts +0 -644
- package/src/__tests__/SstEnvValidator.spec.ts +0 -236
- package/src/__tests__/SstEnvironmentBuilder.spec.ts +0 -397
- package/src/__tests__/credentials.integration.spec.ts +0 -239
- package/src/__tests__/credentials.spec.ts +0 -136
- package/src/__tests__/formatter.spec.ts +0 -268
- package/src/__tests__/sst.spec.ts +0 -437
- package/src/credentials.ts +0 -112
- package/src/formatter.ts +0 -146
- package/src/index.ts +0 -24
- package/src/sst.ts +0 -76
- package/sst-env.d.ts +0 -8
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -18
package/docs/api-reference.md
DELETED
|
@@ -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
|