@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.
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,386 +0,0 @@
1
- import { EnvironmentParser } from '@geekmidas/envkit';
2
- import { z } from 'zod';
3
-
4
- const parser = new EnvironmentParser(process.env as {});
5
- // Example 1: Basic configuration
6
- export function basicExample() {
7
- const config = parser.create((get) => ({
8
- appName: get('APP_NAME').string().default('My App'),
9
- port: get('PORT').string().transform(Number).default(3000),
10
- isDevelopment: get('NODE_ENV')
11
- .string()
12
- .transform((env) => env === 'development'),
13
- }));
14
-
15
- const result = config.parse();
16
-
17
- return result;
18
- }
19
-
20
- // Example 2: Database configuration
21
- export function databaseExample() {
22
- const config = parser.create((get) => ({
23
- database: {
24
- host: get('DB_HOST').string().default('localhost'),
25
- port: get('DB_PORT').string().transform(Number).default(5432),
26
- name: get('DB_NAME').string(),
27
- user: get('DB_USER').string(),
28
- password: get('DB_PASSWORD').string(),
29
- ssl: get('DB_SSL')
30
- .string()
31
- .transform((v) => v === 'true')
32
- .default(false),
33
- poolSize: get('DB_POOL_SIZE')
34
- .string()
35
- .transform(Number)
36
- .int()
37
- .min(1)
38
- .max(100)
39
- .default(10),
40
- },
41
- }));
42
-
43
- return config.parse();
44
- }
45
-
46
- // Example 3: API configuration with validation
47
- export function apiConfigExample() {
48
- const config = parser.create((get) => ({
49
- api: {
50
- baseUrl: get('API_BASE_URL').url(),
51
- key: get('API_KEY').string().min(32),
52
- secret: get('API_SECRET').string().min(64),
53
- timeout: get('API_TIMEOUT').string().transform(Number).default(5000),
54
- retries: get('API_RETRIES').string().transform(Number).default(3),
55
- endpoints: {
56
- users: get('API_ENDPOINT_USERS').string().default('/api/v1/users'),
57
- auth: get('API_ENDPOINT_AUTH').string().default('/api/v1/auth'),
58
- products: get('API_ENDPOINT_PRODUCTS')
59
- .string()
60
- .default('/api/v1/products'),
61
- },
62
- },
63
- }));
64
-
65
- return config.parse();
66
- }
67
-
68
- // Example 4: Feature flags and complex validation
69
- export function featureFlagsExample() {
70
- const config = parser.create((get) => ({
71
- features: {
72
- authentication: get('FEATURE_AUTH')
73
- .string()
74
- .transform((v) => v === 'true')
75
- .default(true),
76
- rateLimit: get('FEATURE_RATE_LIMIT')
77
- .string()
78
- .transform((v) => v === 'true')
79
- .default(true),
80
- cache: get('FEATURE_CACHE')
81
- .string()
82
- .transform((v) => v === 'true')
83
- .default(false),
84
- beta: {
85
- enabled: get('FEATURE_BETA')
86
- .string()
87
- .transform((v) => v === 'true')
88
- .default(false),
89
- allowedUsers: get('FEATURE_BETA_USERS')
90
- .string()
91
- .transform((users) =>
92
- users ? users.split(',').map((u) => u.trim()) : [],
93
- )
94
- .default([]),
95
- },
96
- },
97
- rateLimit: {
98
- windowMs: get('RATE_LIMIT_WINDOW_MS')
99
- .string()
100
- .transform(Number)
101
- .default(60000),
102
- maxRequests: get('RATE_LIMIT_MAX_REQUESTS')
103
- .string()
104
- .transform(Number)
105
- .default(100),
106
- },
107
- }));
108
-
109
- return config.parse();
110
- }
111
-
112
- // Example 5: Email configuration with refinements
113
- export function emailConfigExample() {
114
- const config = parser.create((get) => ({
115
- email: {
116
- provider: get('EMAIL_PROVIDER').enum(['sendgrid', 'mailgun', 'ses']),
117
- apiKey: get('EMAIL_API_KEY').string().min(20),
118
- from: {
119
- name: get('EMAIL_FROM_NAME').string().default('Support Team'),
120
- address: get('EMAIL_FROM_ADDRESS').string().email(),
121
- },
122
- replyTo: get('EMAIL_REPLY_TO').string().email().optional(),
123
- templates: {
124
- welcome: get('EMAIL_TEMPLATE_WELCOME').string().uuid(),
125
- resetPassword: get('EMAIL_TEMPLATE_RESET_PASSWORD').string().uuid(),
126
- invoice: get('EMAIL_TEMPLATE_INVOICE').string().uuid().optional(),
127
- },
128
- smtp: {
129
- host: get('SMTP_HOST').string().optional(),
130
- port: get('SMTP_PORT').string().transform(Number).optional(),
131
- secure: get('SMTP_SECURE')
132
- .string()
133
- .transform((v) => v === 'true')
134
- .default(true),
135
- },
136
- },
137
- }));
138
-
139
- return config.parse();
140
- }
141
-
142
- // Example 6: Multi-environment configuration
143
- export function multiEnvironmentExample() {
144
- const env = process.env.NODE_ENV || 'development';
145
-
146
- // Different config sources based on environment
147
- const configSource = {
148
- ...process.env,
149
- // Override with environment-specific values
150
- ...(env === 'production'
151
- ? {
152
- LOG_LEVEL: 'error',
153
- DEBUG: 'false',
154
- }
155
- : {
156
- LOG_LEVEL: 'debug',
157
- DEBUG: 'true',
158
- }),
159
- };
160
-
161
- const parser = new EnvironmentParser(configSource);
162
-
163
- const config = parser.create((get) => ({
164
- env: get('NODE_ENV')
165
- .enum(['development', 'staging', 'production'])
166
- .default('development'),
167
- logging: {
168
- level: get('LOG_LEVEL').enum([
169
- 'trace',
170
- 'debug',
171
- 'info',
172
- 'warn',
173
- 'error',
174
- 'fatal',
175
- ]),
176
- pretty: get('LOG_PRETTY')
177
- .string()
178
- .transform((v) => v === 'true')
179
- .default(env !== 'production'),
180
- debug: get('DEBUG')
181
- .string()
182
- .transform((v) => v === 'true'),
183
- },
184
- server: {
185
- host: get('HOST').string().default('0.0.0.0'),
186
- port: get('PORT')
187
- .string()
188
- .transform(Number)
189
- .default(env === 'production' ? 80 : 3000),
190
- cors: {
191
- enabled: get('CORS_ENABLED')
192
- .string()
193
- .transform((v) => v === 'true')
194
- .default(true),
195
- origins: get('CORS_ORIGINS')
196
- .string()
197
- .transform((origins) => origins.split(',').map((o) => o.trim()))
198
- .refine((origins) => origins.every((o) => o.startsWith('http')), {
199
- message: 'All CORS origins must be valid URLs',
200
- })
201
- .default(['http://localhost:3000']),
202
- },
203
- },
204
- }));
205
-
206
- return config.parse();
207
- }
208
-
209
- // Example 7: Error handling
210
- export function errorHandlingExample() {
211
- try {
212
- const config = parser
213
- .create((get) => ({
214
- required: {
215
- apiKey: get('API_KEY').string().min(32),
216
- databaseUrl: get('DATABASE_URL').string().url(),
217
- adminEmail: get('ADMIN_EMAIL').string().email(),
218
- },
219
- optional: {
220
- sentryDsn: get('SENTRY_DSN').string().url().optional(),
221
- slackWebhook: get('SLACK_WEBHOOK').string().url().optional(),
222
- },
223
- }))
224
- .parse();
225
-
226
- return config;
227
- } catch (error) {
228
- if (error instanceof z.ZodError) {
229
- error.errors.forEach((err) => {
230
- const _path = err.path.join('.');
231
- });
232
-
233
- // In a real app, you might want to exit
234
- process.exit(1);
235
- }
236
- throw error;
237
- }
238
- }
239
-
240
- // Example 8: Using with dotenv
241
- export function dotenvExample() {
242
- // Load .env file
243
- require('dotenv').config();
244
-
245
- const config = parser.create((get) => ({
246
- app: {
247
- name: get('APP_NAME').string(),
248
- version: get('APP_VERSION')
249
- .string()
250
- .regex(/^\d+\.\d+\.\d+$/),
251
- description: get('APP_DESCRIPTION').string().optional(),
252
- },
253
- secrets: {
254
- jwtSecret: get('JWT_SECRET').string().min(64),
255
- encryptionKey: get('ENCRYPTION_KEY').string().length(32),
256
- apiKeys: get('API_KEYS')
257
- .string()
258
- .transform((keys) => keys.split(',').map((k) => k.trim()))
259
- .pipe(z.array(z.string().min(32))),
260
- },
261
- }));
262
-
263
- return config.parse();
264
- }
265
-
266
- // Example 9: Custom transformations
267
- export function customTransformationsExample() {
268
- const config = parser.create((get) => ({
269
- // Parse JSON
270
- features: get('FEATURES_JSON')
271
- .string()
272
- .transform((str) => JSON.parse(str))
273
- .pipe(z.record(z.boolean())),
274
-
275
- // Parse duration strings
276
- timeouts: {
277
- request: get('TIMEOUT_REQUEST')
278
- .string()
279
- .transform(parseDuration)
280
- .default('30s'),
281
- idle: get('TIMEOUT_IDLE').string().transform(parseDuration).default('5m'),
282
- },
283
-
284
- // Parse memory sizes
285
- limits: {
286
- memory: get('MEMORY_LIMIT')
287
- .string()
288
- .transform(parseMemorySize)
289
- .default('512MB'),
290
- upload: get('UPLOAD_LIMIT')
291
- .string()
292
- .transform(parseMemorySize)
293
- .default('10MB'),
294
- },
295
-
296
- // Complex array parsing
297
- allowedDomains: get('ALLOWED_DOMAINS')
298
- .string()
299
- .transform((domains) =>
300
- domains
301
- .split(',')
302
- .map((d) => d.trim())
303
- .filter(Boolean),
304
- )
305
- .pipe(z.array(z.string().regex(/^[a-z0-9.-]+$/i))),
306
- }));
307
-
308
- return config.parse();
309
- }
310
-
311
- // Helper functions for custom transformations
312
- function parseDuration(duration: string): number {
313
- const match = duration.match(/^(\d+)(ms|s|m|h)$/);
314
- if (!match) throw new Error(`Invalid duration: ${duration}`);
315
-
316
- const [, value, unit] = match;
317
- const multipliers = { ms: 1, s: 1000, m: 60000, h: 3600000 };
318
- return parseInt(value, 10) * multipliers[unit as keyof typeof multipliers];
319
- }
320
-
321
- function parseMemorySize(size: string): number {
322
- const match = size.match(/^(\d+)(B|KB|MB|GB)$/i);
323
- if (!match) throw new Error(`Invalid memory size: ${size}`);
324
-
325
- const [, value, unit] = match;
326
- const multipliers = { B: 1, KB: 1024, MB: 1048576, GB: 1073741824 };
327
- return (
328
- parseInt(value, 10) *
329
- multipliers[unit.toUpperCase() as keyof typeof multipliers]
330
- );
331
- }
332
-
333
- // Example 10: Type-safe configuration module
334
- // config.ts - This is how you'd typically use it in a real app
335
- export const loadConfig = () => {
336
- const parser = new EnvironmentParser(process.env as any);
337
-
338
- return parser
339
- .create((get) => ({
340
- app: {
341
- name: get('APP_NAME').string().default('My Application'),
342
- env: get('NODE_ENV')
343
- .enum(['development', 'staging', 'production'])
344
- .default('development'),
345
- port: get('PORT').string().transform(Number).default(3000),
346
- host: get('HOST').string().default('localhost'),
347
- },
348
- database: {
349
- url: get('DATABASE_URL').string().url(),
350
- maxConnections: get('DB_MAX_CONNECTIONS')
351
- .string()
352
- .transform(Number)
353
- .default(10),
354
- ssl: get('DB_SSL')
355
- .string()
356
- .transform((v) => v === 'true')
357
- .default(false),
358
- },
359
- redis: {
360
- url: get('REDIS_URL').string().url().optional(),
361
- ttl: get('REDIS_TTL').string().transform(Number).default(3600),
362
- },
363
- auth: {
364
- jwtSecret: get('JWT_SECRET').string().min(32),
365
- jwtExpiry: get('JWT_EXPIRY').string().default('7d'),
366
- bcryptRounds: get('BCRYPT_ROUNDS')
367
- .string()
368
- .transform(Number)
369
- .default(10),
370
- },
371
- features: {
372
- signups: get('FEATURE_SIGNUPS')
373
- .string()
374
- .transform((v) => v === 'true')
375
- .default(true),
376
- subscriptions: get('FEATURE_SUBSCRIPTIONS')
377
- .string()
378
- .transform((v) => v === 'true')
379
- .default(false),
380
- },
381
- }))
382
- .parse();
383
- };
384
-
385
- // Export the config for use throughout the app
386
- export const config = loadConfig();
@@ -1,192 +0,0 @@
1
- import snakecase from 'lodash.snakecase';
2
-
3
- /**
4
- * Converts a string to environment variable case format (UPPER_SNAKE_CASE).
5
- * Numbers following underscores are preserved without the underscore.
6
- *
7
- * @param name - The string to convert
8
- * @returns The converted string in environment variable format
9
- *
10
- * @example
11
- * environmentCase('myVariable') // 'MY_VARIABLE'
12
- * environmentCase('apiV2') // 'APIV2'
13
- */
14
- export function environmentCase(name: string): string {
15
- return snakecase(name)
16
- .toUpperCase()
17
- .replace(/_\d+/g, (r) => {
18
- return r.replace('_', '');
19
- });
20
- }
21
-
22
- /**
23
- * A record of environment variable names to their values.
24
- * Values can be primitives or nested records.
25
- */
26
- export interface EnvRecord {
27
- [key: string]: EnvValue;
28
- }
29
-
30
- /**
31
- * Represents a value that can be stored in an environment record.
32
- * Can be a primitive value or a nested record of environment values.
33
- */
34
- export type EnvValue = string | number | boolean | EnvRecord;
35
-
36
- /**
37
- * A resolver function that converts a typed value into environment variables.
38
- *
39
- * @template T - The type of value this resolver handles (without the `type` key)
40
- * @param key - The key name from the input record
41
- * @param value - The value to resolve (without the `type` key)
42
- * @returns A record of environment variable names to their values
43
- */
44
- export type EnvironmentResolver<T = any> = (key: string, value: T) => EnvRecord;
45
-
46
- /**
47
- * A map of type discriminator strings to their resolver functions.
48
- */
49
- export type Resolvers = Record<string, EnvironmentResolver<any>>;
50
-
51
- /**
52
- * Options for configuring the EnvironmentBuilder.
53
- */
54
- export interface EnvironmentBuilderOptions {
55
- /**
56
- * Handler called when a value's type doesn't match any registered resolver.
57
- * Defaults to console.warn.
58
- */
59
- onUnmatchedValue?: (key: string, value: unknown) => void;
60
- }
61
-
62
- /**
63
- * Input value type - either a string or an object with a `type` discriminator.
64
- */
65
- export type InputValue = string | { type: string; [key: string]: unknown };
66
-
67
- /**
68
- * Base type for typed input values with a specific type discriminator.
69
- */
70
- export type TypedInputValue<TType extends string = string> = {
71
- type: TType;
72
- [key: string]: unknown;
73
- };
74
-
75
- /**
76
- * Extracts the `type` string value from an input value.
77
- */
78
- type ExtractType<T> = T extends { type: infer U extends string } ? U : never;
79
-
80
- /**
81
- * Removes the `type` key from an object type.
82
- */
83
- type OmitType<T> = T extends { type: string } ? Omit<T, 'type'> : never;
84
-
85
- /**
86
- * Extracts all unique `type` values from a record (excluding plain strings).
87
- */
88
- type AllTypeValues<TRecord extends Record<string, InputValue>> = {
89
- [K in keyof TRecord]: ExtractType<TRecord[K]>;
90
- }[keyof TRecord];
91
-
92
- /**
93
- * For a given type value, finds the corresponding value type (without `type` key).
94
- */
95
- type ValueForType<
96
- TRecord extends Record<string, InputValue>,
97
- TType extends string,
98
- > = {
99
- [K in keyof TRecord]: TRecord[K] extends { type: TType }
100
- ? OmitType<TRecord[K]>
101
- : never;
102
- }[keyof TRecord];
103
-
104
- /**
105
- * Generates typed resolvers based on the input record.
106
- * Keys are the `type` values, values are resolver functions receiving the value without `type`.
107
- */
108
- export type TypedResolvers<TRecord extends Record<string, InputValue>> = {
109
- [TType in AllTypeValues<TRecord>]: EnvironmentResolver<
110
- ValueForType<TRecord, TType>
111
- >;
112
- };
113
-
114
- /**
115
- * A generic, extensible class for building environment variables from
116
- * objects with type-discriminated values.
117
- *
118
- * @template TRecord - The input record type for type inference
119
- * @template TResolvers - The resolvers type (defaults to TypedResolvers<TRecord>)
120
- *
121
- * @example
122
- * ```typescript
123
- * const env = new EnvironmentBuilder(
124
- * {
125
- * apiKey: { type: 'secret', value: 'xyz' },
126
- * appName: 'my-app'
127
- * },
128
- * {
129
- * // `value` is typed as { value: string } (without `type`)
130
- * secret: (key, value) => ({ [key]: value.value }),
131
- * }
132
- * ).build();
133
- * // { API_KEY: 'xyz', APP_NAME: 'my-app' }
134
- * ```
135
- */
136
- export class EnvironmentBuilder<
137
- TRecord extends Record<string, InputValue> = Record<string, InputValue>,
138
- TResolvers extends Resolvers = TypedResolvers<TRecord>,
139
- > {
140
- private readonly record: TRecord;
141
- private readonly resolvers: TResolvers;
142
- private readonly options: Required<EnvironmentBuilderOptions>;
143
-
144
- constructor(
145
- record: TRecord,
146
- resolvers: TResolvers,
147
- options: EnvironmentBuilderOptions = {},
148
- ) {
149
- this.record = record;
150
- this.resolvers = resolvers;
151
- this.options = {
152
- onUnmatchedValue: options.onUnmatchedValue ?? ((_key, _value) => {}),
153
- };
154
- }
155
-
156
- /**
157
- * Build environment variables from the input record.
158
- *
159
- * - Plain string values are passed through with key transformation
160
- * - Object values with a `type` property are matched against resolvers
161
- * - Resolvers receive values without the `type` key
162
- * - Only root-level keys are transformed to UPPER_SNAKE_CASE
163
- *
164
- * @returns A record of environment variables
165
- */
166
- build(): EnvRecord {
167
- const env: EnvRecord = {};
168
-
169
- for (const [key, value] of Object.entries(this.record)) {
170
- // Handle plain string values
171
- if (typeof value === 'string') {
172
- env[environmentCase(key)] = value;
173
- continue;
174
- }
175
-
176
- // Handle objects with type discriminator
177
- const { type, ...rest } = value;
178
- const resolver = this.resolvers[type];
179
- if (resolver) {
180
- const resolved = resolver(key, rest);
181
- // Transform only root-level keys from resolver output
182
- for (const [resolvedKey, resolvedValue] of Object.entries(resolved)) {
183
- env[environmentCase(resolvedKey)] = resolvedValue;
184
- }
185
- } else {
186
- this.options.onUnmatchedValue(key, value);
187
- }
188
- }
189
-
190
- return env;
191
- }
192
- }