@geekmidas/envkit 0.0.8 → 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 (57) 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-cnxuy7lw.cjs → EnvironmentParser-Bt246UeP.cjs} +1 -1
  13. package/dist/{EnvironmentParser-cnxuy7lw.cjs.map → EnvironmentParser-Bt246UeP.cjs.map} +1 -1
  14. package/dist/{EnvironmentParser-B8--woiB.d.cts → EnvironmentParser-CVWU1ooT.d.mts} +1 -1
  15. package/dist/{EnvironmentParser-STvN_RCc.mjs → EnvironmentParser-c06agx31.mjs} +1 -1
  16. package/dist/{EnvironmentParser-STvN_RCc.mjs.map → EnvironmentParser-c06agx31.mjs.map} +1 -1
  17. package/dist/{EnvironmentParser-C_9v2BDw.d.mts → EnvironmentParser-tV-JjCg7.d.cts} +1 -1
  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 +1 -1
  23. package/dist/SnifferEnvironmentParser.cjs.map +1 -1
  24. package/dist/SnifferEnvironmentParser.d.cts +1 -1
  25. package/dist/SnifferEnvironmentParser.d.mts +1 -1
  26. package/dist/SnifferEnvironmentParser.mjs +1 -1
  27. package/dist/SnifferEnvironmentParser.mjs.map +1 -1
  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 +5 -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 +13 -114
  43. package/dist/sst.cjs.map +1 -1
  44. package/dist/sst.d.cts +14 -93
  45. package/dist/sst.d.mts +14 -93
  46. package/dist/sst.mjs +10 -112
  47. package/dist/sst.mjs.map +1 -1
  48. package/docs/async-secrets-design.md +355 -0
  49. package/package.json +6 -2
  50. package/src/EnvironmentBuilder.ts +196 -0
  51. package/src/SnifferEnvironmentParser.ts +7 -5
  52. package/src/SstEnvironmentBuilder.ts +298 -0
  53. package/src/__tests__/EnvironmentBuilder.spec.ts +274 -0
  54. package/src/__tests__/SstEnvironmentBuilder.spec.ts +373 -0
  55. package/src/__tests__/sst.spec.ts +1 -1
  56. package/src/index.ts +12 -0
  57. package/src/sst.ts +45 -207
@@ -0,0 +1,298 @@
1
+ import {
2
+ type EnvRecord,
3
+ EnvironmentBuilder,
4
+ type EnvironmentBuilderOptions,
5
+ type EnvironmentResolver,
6
+ type InputValue,
7
+ type Resolvers,
8
+ } from './EnvironmentBuilder';
9
+
10
+ /**
11
+ * Enumeration of supported SST (Serverless Stack Toolkit) resource types.
12
+ * Used to identify and process different AWS and SST resources.
13
+ */
14
+ export enum ResourceType {
15
+ // Legacy format (dot notation)
16
+ ApiGatewayV2 = 'sst.aws.ApiGatewayV2',
17
+ Postgres = 'sst.aws.Postgres',
18
+ Function = 'sst.aws.Function',
19
+ Bucket = 'sst.aws.Bucket',
20
+ Vpc = 'sst.aws.Vpc',
21
+ Secret = 'sst.sst.Secret',
22
+
23
+ // Modern format (colon notation)
24
+ SSTSecret = 'sst:sst:Secret',
25
+ SSTFunction = 'sst:sst:Function',
26
+ SSTApiGatewayV2 = 'sst:aws:ApiGatewayV2',
27
+ SSTPostgres = 'sst:aws:Postgres',
28
+ SSTBucket = 'sst:aws:Bucket',
29
+ SnsTopic = 'sst:aws:SnsTopic',
30
+ }
31
+
32
+ /**
33
+ * AWS API Gateway V2 resource type.
34
+ * Represents an HTTP/WebSocket API.
35
+ */
36
+ export type ApiGatewayV2 = {
37
+ type: ResourceType.ApiGatewayV2 | ResourceType.SSTApiGatewayV2;
38
+ url: string;
39
+ };
40
+
41
+ /**
42
+ * PostgreSQL database resource type.
43
+ * Contains all connection details needed to connect to the database.
44
+ */
45
+ export type Postgres = {
46
+ type: ResourceType.Postgres | ResourceType.SSTPostgres;
47
+ database: string;
48
+ host: string;
49
+ password: string;
50
+ port: number;
51
+ username: string;
52
+ };
53
+
54
+ /**
55
+ * AWS Lambda Function resource type.
56
+ */
57
+ export type Function = {
58
+ type: ResourceType.Function | ResourceType.SSTFunction;
59
+ name: string;
60
+ };
61
+
62
+ /**
63
+ * AWS S3 Bucket resource type.
64
+ */
65
+ export type Bucket = {
66
+ type: ResourceType.Bucket | ResourceType.SSTBucket;
67
+ name: string;
68
+ };
69
+
70
+ /**
71
+ * AWS VPC (Virtual Private Cloud) resource type.
72
+ */
73
+ export type Vpc = {
74
+ type: ResourceType.Vpc;
75
+ bastion: string;
76
+ };
77
+
78
+ /**
79
+ * Secret resource type for storing sensitive values.
80
+ */
81
+ export type Secret = {
82
+ type: ResourceType.Secret | ResourceType.SSTSecret;
83
+ value: string;
84
+ };
85
+
86
+ /**
87
+ * AWS SNS Topic resource type.
88
+ */
89
+ export type SnsTopic = {
90
+ type: ResourceType.SnsTopic;
91
+ arn: string;
92
+ };
93
+
94
+ /**
95
+ * Union type of all supported SST resource types.
96
+ */
97
+ export type SstResource =
98
+ | ApiGatewayV2
99
+ | Postgres
100
+ | Function
101
+ | Bucket
102
+ | Vpc
103
+ | Secret
104
+ | SnsTopic;
105
+
106
+ // Value types without the `type` key (for resolver parameters)
107
+ type SecretValue = Omit<Secret, 'type'>;
108
+ type PostgresValue = Omit<Postgres, 'type'>;
109
+ type BucketValue = Omit<Bucket, 'type'>;
110
+ type SnsTopicValue = Omit<SnsTopic, 'type'>;
111
+
112
+ /**
113
+ * Function type for processing a specific resource type into environment variables.
114
+ *
115
+ * @template K - The specific resource type (without `type` key)
116
+ * @param name - The resource name
117
+ * @param value - The resource value (without `type` key)
118
+ * @returns Object mapping environment variable names to values
119
+ */
120
+ export type ResourceProcessor<K> = (name: string, value: K) => EnvRecord;
121
+
122
+ // SST Resource Resolvers (receive values without `type` key)
123
+
124
+ const secretResolver = (name: string, value: SecretValue) => ({
125
+ [name]: value.value,
126
+ });
127
+
128
+ const postgresResolver = (key: string, value: PostgresValue) => ({
129
+ [`${key}Name`]: value.database,
130
+ [`${key}Host`]: value.host,
131
+ [`${key}Password`]: value.password,
132
+ [`${key}Port`]: value.port,
133
+ [`${key}Username`]: value.username,
134
+ });
135
+
136
+ const bucketResolver = (name: string, value: BucketValue) => ({
137
+ [`${name}Name`]: value.name,
138
+ });
139
+
140
+ const topicResolver = (name: string, value: SnsTopicValue) => ({
141
+ [`${name}Arn`]: value.arn,
142
+ });
143
+
144
+ const noopResolver = () => ({});
145
+
146
+ /**
147
+ * Pre-configured resolvers for all SST resource types.
148
+ */
149
+ export const sstResolvers: Resolvers = {
150
+ // Legacy format
151
+ [ResourceType.ApiGatewayV2]: noopResolver,
152
+ [ResourceType.Function]: noopResolver,
153
+ [ResourceType.Vpc]: noopResolver,
154
+ [ResourceType.Secret]: secretResolver,
155
+ [ResourceType.Postgres]: postgresResolver,
156
+ [ResourceType.Bucket]: bucketResolver,
157
+
158
+ // Modern format
159
+ [ResourceType.SSTSecret]: secretResolver,
160
+ [ResourceType.SSTBucket]: bucketResolver,
161
+ [ResourceType.SSTFunction]: noopResolver,
162
+ [ResourceType.SSTPostgres]: postgresResolver,
163
+ [ResourceType.SSTApiGatewayV2]: noopResolver,
164
+ [ResourceType.SnsTopic]: topicResolver,
165
+ };
166
+
167
+ /**
168
+ * All known SST resource type strings.
169
+ */
170
+ type SstResourceTypeString = `${ResourceType}`;
171
+
172
+ /**
173
+ * Extracts the `type` string value from an input value.
174
+ */
175
+ type ExtractType<T> = T extends { type: infer U extends string } ? U : never;
176
+
177
+ /**
178
+ * Removes the `type` key from an object type.
179
+ */
180
+ type OmitType<T> = T extends { type: string } ? Omit<T, 'type'> : never;
181
+
182
+ /**
183
+ * Extracts all unique `type` values from a record (excluding plain strings).
184
+ */
185
+ type AllTypeValues<TRecord extends Record<string, InputValue>> = {
186
+ [K in keyof TRecord]: ExtractType<TRecord[K]>;
187
+ }[keyof TRecord];
188
+
189
+ /**
190
+ * Extracts only the custom (non-SST) type values from a record.
191
+ */
192
+ type CustomTypeValues<TRecord extends Record<string, InputValue>> = Exclude<
193
+ AllTypeValues<TRecord>,
194
+ SstResourceTypeString
195
+ >;
196
+
197
+ /**
198
+ * For a given type value, finds the corresponding value type (without `type` key).
199
+ */
200
+ type ValueForType<
201
+ TRecord extends Record<string, InputValue>,
202
+ TType extends string,
203
+ > = {
204
+ [K in keyof TRecord]: TRecord[K] extends { type: TType }
205
+ ? OmitType<TRecord[K]>
206
+ : never;
207
+ }[keyof TRecord];
208
+
209
+ /**
210
+ * Generates typed resolvers for custom (non-SST) types in the input record.
211
+ */
212
+ type CustomResolvers<TRecord extends Record<string, InputValue>> =
213
+ CustomTypeValues<TRecord> extends never
214
+ ? Resolvers | undefined
215
+ : {
216
+ [TType in CustomTypeValues<TRecord>]: EnvironmentResolver<
217
+ ValueForType<TRecord, TType>
218
+ >;
219
+ };
220
+
221
+ /**
222
+ * SST-specific environment builder with built-in resolvers for all known
223
+ * SST resource types.
224
+ *
225
+ * Wraps the generic EnvironmentBuilder with pre-configured SST resolvers.
226
+ *
227
+ * @template TRecord - The input record type for type inference
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * const env = new SstEnvironmentBuilder({
232
+ * database: { type: 'sst:aws:Postgres', host: '...', ... },
233
+ * apiKey: { type: 'sst:sst:Secret', value: 'secret' },
234
+ * appName: 'my-app',
235
+ * }).build();
236
+ *
237
+ * // With custom resolvers (typed based on input)
238
+ * const env = new SstEnvironmentBuilder(
239
+ * {
240
+ * database: postgresResource,
241
+ * custom: { type: 'my-custom' as const, data: 'foo' },
242
+ * },
243
+ * {
244
+ * // TypeScript requires 'my-custom' resolver with typed value
245
+ * 'my-custom': (key, value) => ({ [`${key}Data`]: value.data }),
246
+ * }
247
+ * ).build();
248
+ * ```
249
+ */
250
+ export class SstEnvironmentBuilder<
251
+ TRecord extends Record<string, SstResource | InputValue | string>,
252
+ > {
253
+ private readonly builder: EnvironmentBuilder<
254
+ Record<string, InputValue>,
255
+ Resolvers
256
+ >;
257
+
258
+ /**
259
+ * Create a new SST environment builder.
260
+ *
261
+ * @param record - Object containing SST resources, custom resources, and/or string values
262
+ * @param additionalResolvers - Optional custom resolvers (typed based on custom types in record)
263
+ * @param options - Optional configuration options
264
+ */
265
+ constructor(
266
+ record: TRecord,
267
+ additionalResolvers?: CustomResolvers<TRecord>,
268
+ options?: EnvironmentBuilderOptions,
269
+ ) {
270
+ // Merge resolvers with custom ones taking precedence
271
+ const mergedResolvers: Resolvers = additionalResolvers
272
+ ? { ...sstResolvers, ...additionalResolvers }
273
+ : sstResolvers;
274
+
275
+ this.builder = new EnvironmentBuilder(
276
+ record as Record<string, InputValue>,
277
+ mergedResolvers,
278
+ options,
279
+ );
280
+ }
281
+
282
+ /**
283
+ * Build environment variables from the input record.
284
+ *
285
+ * @returns A record of environment variables
286
+ */
287
+ build(): EnvRecord {
288
+ return this.builder.build();
289
+ }
290
+ }
291
+
292
+ // Re-export useful types
293
+ export { environmentCase } from './EnvironmentBuilder';
294
+ export type {
295
+ EnvRecord,
296
+ EnvValue,
297
+ EnvironmentBuilderOptions,
298
+ } from './EnvironmentBuilder';
@@ -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
+ });