@geekmidas/envkit 0.0.7 → 0.1.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 (74) hide show
  1. package/README.md +228 -174
  2. package/dist/EnvironmentBuilder-DHfDXJUm.d.mts +131 -0
  3. package/dist/EnvironmentBuilder-DfmYRBm-.mjs +83 -0
  4. package/dist/EnvironmentBuilder-DfmYRBm-.mjs.map +1 -0
  5. package/dist/EnvironmentBuilder-W2wku49g.cjs +95 -0
  6. package/dist/EnvironmentBuilder-W2wku49g.cjs.map +1 -0
  7. package/dist/EnvironmentBuilder-Xuf2Dd9u.d.cts +131 -0
  8. package/dist/EnvironmentBuilder.cjs +4 -0
  9. package/dist/EnvironmentBuilder.d.cts +2 -0
  10. package/dist/EnvironmentBuilder.d.mts +2 -0
  11. package/dist/EnvironmentBuilder.mjs +3 -0
  12. package/dist/{EnvironmentParser-BDPDLv6i.cjs → EnvironmentParser-Bt246UeP.cjs} +46 -3
  13. package/dist/EnvironmentParser-Bt246UeP.cjs.map +1 -0
  14. package/dist/{EnvironmentParser-C-arQEHQ.d.mts → EnvironmentParser-CVWU1ooT.d.mts} +40 -2
  15. package/dist/{EnvironmentParser-CQUOGqc0.mjs → EnvironmentParser-c06agx31.mjs} +46 -3
  16. package/dist/EnvironmentParser-c06agx31.mjs.map +1 -0
  17. package/dist/{EnvironmentParser-X4h2Vp4r.d.cts → EnvironmentParser-tV-JjCg7.d.cts} +40 -2
  18. package/dist/EnvironmentParser.cjs +1 -1
  19. package/dist/EnvironmentParser.d.cts +1 -1
  20. package/dist/EnvironmentParser.d.mts +1 -1
  21. package/dist/EnvironmentParser.mjs +1 -1
  22. package/dist/SnifferEnvironmentParser.cjs +140 -0
  23. package/dist/SnifferEnvironmentParser.cjs.map +1 -0
  24. package/dist/SnifferEnvironmentParser.d.cts +50 -0
  25. package/dist/SnifferEnvironmentParser.d.mts +50 -0
  26. package/dist/SnifferEnvironmentParser.mjs +139 -0
  27. package/dist/SnifferEnvironmentParser.mjs.map +1 -0
  28. package/dist/SstEnvironmentBuilder-BuFw1hCe.cjs +125 -0
  29. package/dist/SstEnvironmentBuilder-BuFw1hCe.cjs.map +1 -0
  30. package/dist/SstEnvironmentBuilder-CjURMGjW.d.mts +177 -0
  31. package/dist/SstEnvironmentBuilder-D4oSo_KX.d.cts +177 -0
  32. package/dist/SstEnvironmentBuilder-DEa3lTUB.mjs +108 -0
  33. package/dist/SstEnvironmentBuilder-DEa3lTUB.mjs.map +1 -0
  34. package/dist/SstEnvironmentBuilder.cjs +7 -0
  35. package/dist/SstEnvironmentBuilder.d.cts +3 -0
  36. package/dist/SstEnvironmentBuilder.d.mts +3 -0
  37. package/dist/SstEnvironmentBuilder.mjs +4 -0
  38. package/dist/index.cjs +6 -2
  39. package/dist/index.d.cts +3 -2
  40. package/dist/index.d.mts +3 -2
  41. package/dist/index.mjs +3 -2
  42. package/dist/sst.cjs +30 -4
  43. package/dist/sst.cjs.map +1 -0
  44. package/dist/sst.d.cts +15 -93
  45. package/dist/sst.d.mts +15 -93
  46. package/dist/sst.mjs +26 -2
  47. package/dist/sst.mjs.map +1 -0
  48. package/docs/async-secrets-design.md +355 -0
  49. package/package.json +11 -2
  50. package/src/EnvironmentBuilder.ts +196 -0
  51. package/src/EnvironmentParser.ts +51 -2
  52. package/src/SnifferEnvironmentParser.ts +209 -0
  53. package/src/SstEnvironmentBuilder.ts +298 -0
  54. package/src/__tests__/EnvironmentBuilder.spec.ts +274 -0
  55. package/src/__tests__/EnvironmentParser.spec.ts +147 -0
  56. package/src/__tests__/SnifferEnvironmentParser.spec.ts +332 -0
  57. package/src/__tests__/SstEnvironmentBuilder.spec.ts +373 -0
  58. package/src/__tests__/sst.spec.ts +1 -1
  59. package/src/index.ts +13 -1
  60. package/src/sst.ts +45 -207
  61. package/dist/__tests__/ConfigParser.spec.cjs +0 -323
  62. package/dist/__tests__/ConfigParser.spec.d.cts +0 -1
  63. package/dist/__tests__/ConfigParser.spec.d.mts +0 -1
  64. package/dist/__tests__/ConfigParser.spec.mjs +0 -322
  65. package/dist/__tests__/EnvironmentParser.spec.cjs +0 -422
  66. package/dist/__tests__/EnvironmentParser.spec.d.cts +0 -1
  67. package/dist/__tests__/EnvironmentParser.spec.d.mts +0 -1
  68. package/dist/__tests__/EnvironmentParser.spec.mjs +0 -421
  69. package/dist/__tests__/sst.spec.cjs +0 -305
  70. package/dist/__tests__/sst.spec.d.cts +0 -1
  71. package/dist/__tests__/sst.spec.d.mts +0 -1
  72. package/dist/__tests__/sst.spec.mjs +0 -304
  73. package/dist/sst-BSxwaAdz.cjs +0 -146
  74. package/dist/sst-CQhO0S6y.mjs +0 -128
@@ -0,0 +1,274 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { EnvironmentBuilder, environmentCase } from '../EnvironmentBuilder';
4
+
5
+ describe('environmentCase', () => {
6
+ it('should convert camelCase to UPPER_SNAKE_CASE', () => {
7
+ expect(environmentCase('myVariable')).toBe('MY_VARIABLE');
8
+ expect(environmentCase('apiUrl')).toBe('API_URL');
9
+ expect(environmentCase('databaseName')).toBe('DATABASE_NAME');
10
+ });
11
+
12
+ it('should handle already snake_case', () => {
13
+ expect(environmentCase('my_variable')).toBe('MY_VARIABLE');
14
+ expect(environmentCase('api_url')).toBe('API_URL');
15
+ });
16
+
17
+ it('should remove underscore directly before numbers', () => {
18
+ // The regex /_\d+/g only removes underscores that are directly followed by digits
19
+ expect(environmentCase('api_v2')).toBe('API_V2');
20
+ expect(environmentCase('value_123')).toBe('VALUE123');
21
+ expect(environmentCase('my_var_2')).toBe('MY_VAR2');
22
+ });
23
+
24
+ it('should handle single words', () => {
25
+ expect(environmentCase('name')).toBe('NAME');
26
+ expect(environmentCase('port')).toBe('PORT');
27
+ });
28
+ });
29
+
30
+ describe('EnvironmentBuilder', () => {
31
+ describe('basic functionality', () => {
32
+ it('should pass through plain string values with key transformation', () => {
33
+ const env = new EnvironmentBuilder(
34
+ {
35
+ appName: 'my-app',
36
+ nodeEnv: 'production',
37
+ },
38
+ {},
39
+ ).build();
40
+
41
+ expect(env).toEqual({
42
+ APP_NAME: 'my-app',
43
+ NODE_ENV: 'production',
44
+ });
45
+ });
46
+
47
+ it('should resolve object values using type-based resolvers', () => {
48
+ const env = new EnvironmentBuilder(
49
+ {
50
+ apiKey: { type: 'secret', value: 'xyz' },
51
+ },
52
+ {
53
+ secret: (key, value: { type: string; value: string }) => ({
54
+ [key]: value.value,
55
+ }),
56
+ },
57
+ ).build();
58
+
59
+ expect(env).toEqual({
60
+ API_KEY: 'xyz',
61
+ });
62
+ });
63
+
64
+ it('should transform resolver output keys to UPPER_SNAKE_CASE', () => {
65
+ const env = new EnvironmentBuilder(
66
+ {
67
+ auth: { type: 'auth0', domain: 'example.auth0.com', clientId: 'abc' },
68
+ },
69
+ {
70
+ auth0: (
71
+ key,
72
+ value: { type: string; domain: string; clientId: string },
73
+ ) => ({
74
+ [`${key}Domain`]: value.domain,
75
+ [`${key}ClientId`]: value.clientId,
76
+ }),
77
+ },
78
+ ).build();
79
+
80
+ expect(env).toEqual({
81
+ AUTH_DOMAIN: 'example.auth0.com',
82
+ AUTH_CLIENT_ID: 'abc',
83
+ });
84
+ });
85
+
86
+ it('should handle mixed string and object values', () => {
87
+ const env = new EnvironmentBuilder(
88
+ {
89
+ appName: 'my-app',
90
+ apiKey: { type: 'secret', value: 'secret-key' },
91
+ nodeEnv: 'production',
92
+ },
93
+ {
94
+ secret: (key, value: { type: string; value: string }) => ({
95
+ [key]: value.value,
96
+ }),
97
+ },
98
+ ).build();
99
+
100
+ expect(env).toEqual({
101
+ APP_NAME: 'my-app',
102
+ API_KEY: 'secret-key',
103
+ NODE_ENV: 'production',
104
+ });
105
+ });
106
+ });
107
+
108
+ describe('nested values', () => {
109
+ it('should support nested object values', () => {
110
+ const env = new EnvironmentBuilder(
111
+ {
112
+ database: {
113
+ type: 'multi-db',
114
+ primary: 'pg://primary',
115
+ replica: 'pg://replica',
116
+ },
117
+ },
118
+ {
119
+ 'multi-db': (
120
+ key,
121
+ value: { type: string; primary: string; replica: string },
122
+ ) => ({
123
+ [key]: {
124
+ primary: value.primary,
125
+ replica: value.replica,
126
+ },
127
+ }),
128
+ },
129
+ ).build();
130
+
131
+ expect(env).toEqual({
132
+ DATABASE: {
133
+ primary: 'pg://primary',
134
+ replica: 'pg://replica',
135
+ },
136
+ });
137
+ });
138
+
139
+ it('should not transform nested object keys', () => {
140
+ const env = new EnvironmentBuilder(
141
+ {
142
+ config: { type: 'nested', camelCase: 'value', snake_case: 'value2' },
143
+ },
144
+ {
145
+ nested: (
146
+ key,
147
+ value: { type: string; camelCase: string; snake_case: string },
148
+ ) => ({
149
+ [key]: {
150
+ camelCase: value.camelCase,
151
+ snake_case: value.snake_case,
152
+ },
153
+ }),
154
+ },
155
+ ).build();
156
+
157
+ expect(env).toEqual({
158
+ CONFIG: {
159
+ camelCase: 'value',
160
+ snake_case: 'value2',
161
+ },
162
+ });
163
+ });
164
+ });
165
+
166
+ describe('unmatched values', () => {
167
+ it('should call onUnmatchedValue for objects without matching resolver', () => {
168
+ const onUnmatchedValue = vi.fn();
169
+
170
+ new EnvironmentBuilder(
171
+ {
172
+ unknown: { type: 'unknown-type', data: 'test' },
173
+ },
174
+ {},
175
+ { onUnmatchedValue },
176
+ ).build();
177
+
178
+ expect(onUnmatchedValue).toHaveBeenCalledWith('unknown', {
179
+ type: 'unknown-type',
180
+ data: 'test',
181
+ });
182
+ });
183
+
184
+ it('should default to console.warn for unmatched values', () => {
185
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
186
+
187
+ new EnvironmentBuilder(
188
+ {
189
+ unknown: { type: 'unknown-type', data: 'test' },
190
+ },
191
+ {},
192
+ ).build();
193
+
194
+ expect(warnSpy).toHaveBeenCalledWith(
195
+ 'No resolver found for key "unknown":',
196
+ {
197
+ value: { type: 'unknown-type', data: 'test' },
198
+ },
199
+ );
200
+
201
+ warnSpy.mockRestore();
202
+ });
203
+ });
204
+
205
+ describe('value types', () => {
206
+ it('should support number values', () => {
207
+ const env = new EnvironmentBuilder(
208
+ {
209
+ port: { type: 'port', value: 3000 },
210
+ },
211
+ {
212
+ port: (key, value: { type: string; value: number }) => ({
213
+ [key]: value.value,
214
+ }),
215
+ },
216
+ ).build();
217
+
218
+ expect(env).toEqual({
219
+ PORT: 3000,
220
+ });
221
+ });
222
+
223
+ it('should support boolean values', () => {
224
+ const env = new EnvironmentBuilder(
225
+ {
226
+ enabled: { type: 'flag', value: true },
227
+ },
228
+ {
229
+ flag: (key, value: { type: string; value: boolean }) => ({
230
+ [key]: value.value,
231
+ }),
232
+ },
233
+ ).build();
234
+
235
+ expect(env).toEqual({
236
+ ENABLED: true,
237
+ });
238
+ });
239
+ });
240
+
241
+ describe('multiple resolvers', () => {
242
+ it('should use the correct resolver for each type', () => {
243
+ const env = new EnvironmentBuilder(
244
+ {
245
+ secret: { type: 'secret', value: 'my-secret' },
246
+ database: { type: 'postgres', host: 'localhost', port: 5432 },
247
+ bucket: { type: 'bucket', name: 'my-bucket' },
248
+ },
249
+ {
250
+ secret: (key, value: { type: string; value: string }) => ({
251
+ [key]: value.value,
252
+ }),
253
+ postgres: (
254
+ key,
255
+ value: { type: string; host: string; port: number },
256
+ ) => ({
257
+ [`${key}Host`]: value.host,
258
+ [`${key}Port`]: value.port,
259
+ }),
260
+ bucket: (key, value: { type: string; name: string }) => ({
261
+ [`${key}Name`]: value.name,
262
+ }),
263
+ },
264
+ ).build();
265
+
266
+ expect(env).toEqual({
267
+ SECRET: 'my-secret',
268
+ DATABASE_HOST: 'localhost',
269
+ DATABASE_PORT: 5432,
270
+ BUCKET_NAME: 'my-bucket',
271
+ });
272
+ });
273
+ });
274
+ });
@@ -689,4 +689,151 @@ describe('EnvironmentParser', () => {
689
689
  expect(_typeCheck2).toBe(true);
690
690
  });
691
691
  });
692
+
693
+ describe('Environment variable tracking', () => {
694
+ it('should track accessed environment variables', () => {
695
+ const env = { APP_NAME: 'Test App', PORT: '3000' };
696
+ const parser = new EnvironmentParser(env);
697
+
698
+ const config = parser.create((get) => ({
699
+ appName: get('APP_NAME').string(),
700
+ port: get('PORT').string().transform(Number),
701
+ }));
702
+
703
+ const envVars = config.getEnvironmentVariables();
704
+
705
+ expect(envVars).toEqual(['APP_NAME', 'PORT']);
706
+ });
707
+
708
+ it('should track variables even when not parsed', () => {
709
+ const env = {};
710
+ const parser = new EnvironmentParser(env);
711
+
712
+ const config = parser.create((get) => ({
713
+ database: get('DATABASE_URL').string().optional(),
714
+ redis: get('REDIS_URL').string().optional(),
715
+ }));
716
+
717
+ // Should track even without calling parse()
718
+ const envVars = config.getEnvironmentVariables();
719
+
720
+ expect(envVars).toEqual(['DATABASE_URL', 'REDIS_URL']);
721
+ });
722
+
723
+ it('should track variables in nested configurations', () => {
724
+ const env = {
725
+ DB_HOST: 'localhost',
726
+ DB_PORT: '5432',
727
+ API_KEY: 'secret',
728
+ };
729
+ const parser = new EnvironmentParser(env);
730
+
731
+ const config = parser.create((get) => ({
732
+ database: {
733
+ host: get('DB_HOST').string(),
734
+ port: get('DB_PORT').string().transform(Number),
735
+ },
736
+ api: {
737
+ key: get('API_KEY').string(),
738
+ },
739
+ }));
740
+
741
+ const envVars = config.getEnvironmentVariables();
742
+
743
+ expect(envVars).toEqual(['API_KEY', 'DB_HOST', 'DB_PORT']);
744
+ });
745
+
746
+ it('should return sorted environment variable names', () => {
747
+ const env = {};
748
+ const parser = new EnvironmentParser(env);
749
+
750
+ const config = parser.create((get) => ({
751
+ zValue: get('Z_VALUE').string().optional(),
752
+ aValue: get('A_VALUE').string().optional(),
753
+ mValue: get('M_VALUE').string().optional(),
754
+ }));
755
+
756
+ const envVars = config.getEnvironmentVariables();
757
+
758
+ // Should be sorted alphabetically
759
+ expect(envVars).toEqual(['A_VALUE', 'M_VALUE', 'Z_VALUE']);
760
+ });
761
+
762
+ it('should deduplicate environment variable names', () => {
763
+ const env = { SHARED_VAR: 'value' };
764
+ const parser = new EnvironmentParser(env);
765
+
766
+ const config = parser.create((get) => ({
767
+ value1: get('SHARED_VAR').string(),
768
+ value2: get('SHARED_VAR').string(),
769
+ value3: get('SHARED_VAR').string(),
770
+ }));
771
+
772
+ const envVars = config.getEnvironmentVariables();
773
+
774
+ // Should only appear once despite being accessed 3 times
775
+ expect(envVars).toEqual(['SHARED_VAR']);
776
+ });
777
+
778
+ it('should track variables with default values', () => {
779
+ const env = {};
780
+ const parser = new EnvironmentParser(env);
781
+
782
+ const config = parser.create((get) => ({
783
+ port: get('PORT').string().default('3000'),
784
+ host: get('HOST').string().default('localhost'),
785
+ }));
786
+
787
+ const envVars = config.getEnvironmentVariables();
788
+
789
+ // Should track even when defaults are used
790
+ expect(envVars).toEqual(['HOST', 'PORT']);
791
+ });
792
+
793
+ it('should work with empty configuration', () => {
794
+ const env = {};
795
+ const parser = new EnvironmentParser(env);
796
+
797
+ const config = parser.create(() => ({}));
798
+
799
+ const envVars = config.getEnvironmentVariables();
800
+
801
+ expect(envVars).toEqual([]);
802
+ });
803
+
804
+ it('should track variables accessed through coerce', () => {
805
+ const env = { NUM_WORKERS: '4', TIMEOUT: '30000' };
806
+ const parser = new EnvironmentParser(env);
807
+
808
+ const config = parser.create((get) => ({
809
+ workers: get('NUM_WORKERS').coerce.number(),
810
+ timeout: get('TIMEOUT').coerce.number(),
811
+ }));
812
+
813
+ const envVars = config.getEnvironmentVariables();
814
+
815
+ expect(envVars).toEqual(['NUM_WORKERS', 'TIMEOUT']);
816
+ });
817
+
818
+ it('should track variables with complex transformations', () => {
819
+ const env = {
820
+ ALLOWED_ORIGINS: 'http://localhost,https://example.com',
821
+ FEATURE_FLAGS: 'auth,cache',
822
+ };
823
+ const parser = new EnvironmentParser(env);
824
+
825
+ const config = parser.create((get) => ({
826
+ origins: get('ALLOWED_ORIGINS')
827
+ .string()
828
+ .transform((v) => v.split(',')),
829
+ features: get('FEATURE_FLAGS')
830
+ .string()
831
+ .transform((v) => v.split(',')),
832
+ }));
833
+
834
+ const envVars = config.getEnvironmentVariables();
835
+
836
+ expect(envVars).toEqual(['ALLOWED_ORIGINS', 'FEATURE_FLAGS']);
837
+ });
838
+ });
692
839
  });