@geekmidas/envkit 1.1.1 → 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.
- 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 -97
- 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
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { createCipheriv, randomBytes } from 'node:crypto';
|
|
2
|
-
import vm from 'node:vm';
|
|
3
|
-
import { describe, expect, it } from 'vitest';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Helper to encrypt secrets (mirrors CLI encryption logic)
|
|
7
|
-
*/
|
|
8
|
-
function encryptSecrets(secrets: Record<string, string>) {
|
|
9
|
-
const masterKey = randomBytes(32);
|
|
10
|
-
const iv = randomBytes(12);
|
|
11
|
-
const plaintext = JSON.stringify(secrets);
|
|
12
|
-
|
|
13
|
-
const cipher = createCipheriv('aes-256-gcm', masterKey, iv);
|
|
14
|
-
const ciphertext = Buffer.concat([
|
|
15
|
-
cipher.update(plaintext, 'utf-8'),
|
|
16
|
-
cipher.final(),
|
|
17
|
-
]);
|
|
18
|
-
const authTag = cipher.getAuthTag();
|
|
19
|
-
const combined = Buffer.concat([ciphertext, authTag]);
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
encrypted: combined.toString('base64'),
|
|
23
|
-
iv: iv.toString('hex'),
|
|
24
|
-
masterKey: masterKey.toString('hex'),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Simulates the credentials.ts module code that runs at runtime
|
|
30
|
-
*/
|
|
31
|
-
const credentialsModuleCode = `
|
|
32
|
-
const { createDecipheriv } = require('node:crypto');
|
|
33
|
-
|
|
34
|
-
const AUTH_TAG_LENGTH = 16;
|
|
35
|
-
|
|
36
|
-
function decryptCredentials(encrypted, iv, masterKey) {
|
|
37
|
-
const key = Buffer.from(masterKey, 'hex');
|
|
38
|
-
const ivBuffer = Buffer.from(iv, 'hex');
|
|
39
|
-
const combined = Buffer.from(encrypted, 'base64');
|
|
40
|
-
|
|
41
|
-
const ciphertext = combined.subarray(0, -AUTH_TAG_LENGTH);
|
|
42
|
-
const authTag = combined.subarray(-AUTH_TAG_LENGTH);
|
|
43
|
-
|
|
44
|
-
const decipher = createDecipheriv('aes-256-gcm', key, ivBuffer);
|
|
45
|
-
decipher.setAuthTag(authTag);
|
|
46
|
-
|
|
47
|
-
const plaintext = Buffer.concat([
|
|
48
|
-
decipher.update(ciphertext),
|
|
49
|
-
decipher.final(),
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
return JSON.parse(plaintext.toString('utf-8'));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// This simulates runtime behavior
|
|
56
|
-
const Credentials = (() => {
|
|
57
|
-
if (typeof __GKM_ENCRYPTED_CREDENTIALS__ === 'undefined' ||
|
|
58
|
-
typeof __GKM_CREDENTIALS_IV__ === 'undefined') {
|
|
59
|
-
return {};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const masterKey = process.env.GKM_MASTER_KEY;
|
|
63
|
-
if (!masterKey) {
|
|
64
|
-
return {};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
return decryptCredentials(
|
|
69
|
-
__GKM_ENCRYPTED_CREDENTIALS__,
|
|
70
|
-
__GKM_CREDENTIALS_IV__,
|
|
71
|
-
masterKey
|
|
72
|
-
);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
return {};
|
|
75
|
-
}
|
|
76
|
-
})();
|
|
77
|
-
|
|
78
|
-
module.exports = { Credentials };
|
|
79
|
-
`;
|
|
80
|
-
|
|
81
|
-
describe('Credentials runtime integration', () => {
|
|
82
|
-
it('should decrypt credentials when globals and master key are set', () => {
|
|
83
|
-
const secrets = {
|
|
84
|
-
DATABASE_URL: 'postgresql://user:pass@localhost/db',
|
|
85
|
-
API_KEY: 'sk_test_123456',
|
|
86
|
-
WEBHOOK_SECRET: 'whsec_abc123',
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
90
|
-
|
|
91
|
-
// Create a VM context with the build-time injected globals
|
|
92
|
-
const context = vm.createContext({
|
|
93
|
-
__GKM_ENCRYPTED_CREDENTIALS__: encrypted,
|
|
94
|
-
__GKM_CREDENTIALS_IV__: iv,
|
|
95
|
-
process: {
|
|
96
|
-
env: {
|
|
97
|
-
GKM_MASTER_KEY: masterKey,
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
require: require,
|
|
101
|
-
Buffer: Buffer,
|
|
102
|
-
module: { exports: {} },
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Run the credentials module code
|
|
106
|
-
vm.runInContext(credentialsModuleCode, context);
|
|
107
|
-
|
|
108
|
-
// Get the exported Credentials
|
|
109
|
-
const { Credentials } = context.module.exports;
|
|
110
|
-
|
|
111
|
-
expect(Credentials).toEqual(secrets);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should return empty object when globals are not defined', () => {
|
|
115
|
-
const context = vm.createContext({
|
|
116
|
-
process: { env: {} },
|
|
117
|
-
require: require,
|
|
118
|
-
Buffer: Buffer,
|
|
119
|
-
module: { exports: {} },
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
vm.runInContext(credentialsModuleCode, context);
|
|
123
|
-
|
|
124
|
-
const { Credentials } = context.module.exports;
|
|
125
|
-
expect(Credentials).toEqual({});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should return empty object when master key is missing', () => {
|
|
129
|
-
const secrets = { KEY: 'value' };
|
|
130
|
-
const { encrypted, iv } = encryptSecrets(secrets);
|
|
131
|
-
|
|
132
|
-
const context = vm.createContext({
|
|
133
|
-
__GKM_ENCRYPTED_CREDENTIALS__: encrypted,
|
|
134
|
-
__GKM_CREDENTIALS_IV__: iv,
|
|
135
|
-
process: { env: {} }, // No GKM_MASTER_KEY
|
|
136
|
-
require: require,
|
|
137
|
-
Buffer: Buffer,
|
|
138
|
-
module: { exports: {} },
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
vm.runInContext(credentialsModuleCode, context);
|
|
142
|
-
|
|
143
|
-
const { Credentials } = context.module.exports;
|
|
144
|
-
expect(Credentials).toEqual({});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should return empty object when master key is wrong', () => {
|
|
148
|
-
const secrets = { KEY: 'value' };
|
|
149
|
-
const { encrypted, iv } = encryptSecrets(secrets);
|
|
150
|
-
|
|
151
|
-
const context = vm.createContext({
|
|
152
|
-
__GKM_ENCRYPTED_CREDENTIALS__: encrypted,
|
|
153
|
-
__GKM_CREDENTIALS_IV__: iv,
|
|
154
|
-
process: {
|
|
155
|
-
env: {
|
|
156
|
-
GKM_MASTER_KEY: '0'.repeat(64), // Wrong key
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
require: require,
|
|
160
|
-
Buffer: Buffer,
|
|
161
|
-
module: { exports: {} },
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
vm.runInContext(credentialsModuleCode, context);
|
|
165
|
-
|
|
166
|
-
const { Credentials } = context.module.exports;
|
|
167
|
-
expect(Credentials).toEqual({});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should handle complex secrets with special characters', () => {
|
|
171
|
-
const secrets = {
|
|
172
|
-
PRIVATE_KEY:
|
|
173
|
-
'-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----',
|
|
174
|
-
JSON_CONFIG: '{"key": "value", "nested": {"a": 1}}',
|
|
175
|
-
UNICODE: '你好世界 🔐',
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
179
|
-
|
|
180
|
-
const context = vm.createContext({
|
|
181
|
-
__GKM_ENCRYPTED_CREDENTIALS__: encrypted,
|
|
182
|
-
__GKM_CREDENTIALS_IV__: iv,
|
|
183
|
-
process: {
|
|
184
|
-
env: { GKM_MASTER_KEY: masterKey },
|
|
185
|
-
},
|
|
186
|
-
require: require,
|
|
187
|
-
Buffer: Buffer,
|
|
188
|
-
module: { exports: {} },
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
vm.runInContext(credentialsModuleCode, context);
|
|
192
|
-
|
|
193
|
-
const { Credentials } = context.module.exports;
|
|
194
|
-
expect(Credentials).toEqual(secrets);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('should work with EnvironmentParser pattern', () => {
|
|
198
|
-
const secrets = {
|
|
199
|
-
DATABASE_URL: 'postgresql://user:pass@localhost/db',
|
|
200
|
-
PORT: '3000',
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
204
|
-
|
|
205
|
-
// Simulate: {...process.env, ...Credentials}
|
|
206
|
-
const processEnv = {
|
|
207
|
-
NODE_ENV: 'production',
|
|
208
|
-
HOST: 'localhost',
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const context = vm.createContext({
|
|
212
|
-
__GKM_ENCRYPTED_CREDENTIALS__: encrypted,
|
|
213
|
-
__GKM_CREDENTIALS_IV__: iv,
|
|
214
|
-
process: {
|
|
215
|
-
env: {
|
|
216
|
-
...processEnv,
|
|
217
|
-
GKM_MASTER_KEY: masterKey,
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
require: require,
|
|
221
|
-
Buffer: Buffer,
|
|
222
|
-
module: { exports: {} },
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
vm.runInContext(credentialsModuleCode, context);
|
|
226
|
-
|
|
227
|
-
const { Credentials } = context.module.exports;
|
|
228
|
-
|
|
229
|
-
// Simulate what user code does
|
|
230
|
-
const combinedEnv = { ...processEnv, ...Credentials };
|
|
231
|
-
|
|
232
|
-
expect(combinedEnv).toEqual({
|
|
233
|
-
NODE_ENV: 'production',
|
|
234
|
-
HOST: 'localhost',
|
|
235
|
-
DATABASE_URL: 'postgresql://user:pass@localhost/db',
|
|
236
|
-
PORT: '3000',
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { createCipheriv, randomBytes } from 'node:crypto';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { decryptCredentials } from '../credentials';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Helper to encrypt secrets (mirrors CLI encryption logic)
|
|
7
|
-
*/
|
|
8
|
-
function encryptSecrets(secrets: Record<string, string>) {
|
|
9
|
-
const masterKey = randomBytes(32);
|
|
10
|
-
const iv = randomBytes(12);
|
|
11
|
-
const plaintext = JSON.stringify(secrets);
|
|
12
|
-
|
|
13
|
-
const cipher = createCipheriv('aes-256-gcm', masterKey, iv);
|
|
14
|
-
const ciphertext = Buffer.concat([
|
|
15
|
-
cipher.update(plaintext, 'utf-8'),
|
|
16
|
-
cipher.final(),
|
|
17
|
-
]);
|
|
18
|
-
const authTag = cipher.getAuthTag();
|
|
19
|
-
const combined = Buffer.concat([ciphertext, authTag]);
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
encrypted: combined.toString('base64'),
|
|
23
|
-
iv: iv.toString('hex'),
|
|
24
|
-
masterKey: masterKey.toString('hex'),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe('decryptCredentials', () => {
|
|
29
|
-
it('should decrypt credentials from encrypted payload', () => {
|
|
30
|
-
const secrets = {
|
|
31
|
-
DATABASE_URL: 'postgresql://user:pass@localhost/db',
|
|
32
|
-
API_KEY: 'sk_test_123456',
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
36
|
-
const decrypted = decryptCredentials(encrypted, iv, masterKey);
|
|
37
|
-
|
|
38
|
-
expect(decrypted).toEqual(secrets);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should handle empty secrets', () => {
|
|
42
|
-
const secrets = {};
|
|
43
|
-
|
|
44
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
45
|
-
const decrypted = decryptCredentials(encrypted, iv, masterKey);
|
|
46
|
-
|
|
47
|
-
expect(decrypted).toEqual({});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should handle secrets with special characters', () => {
|
|
51
|
-
const secrets = {
|
|
52
|
-
PASSWORD: 'p@ss/word!#$%^&*(){}[]|\\:";\'<>,.?/',
|
|
53
|
-
WEBHOOK_URL: 'https://api.example.com/webhook?token=abc&verify=true',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
57
|
-
const decrypted = decryptCredentials(encrypted, iv, masterKey);
|
|
58
|
-
|
|
59
|
-
expect(decrypted).toEqual(secrets);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should handle secrets with unicode characters', () => {
|
|
63
|
-
const secrets = {
|
|
64
|
-
MESSAGE: '你好世界 🔐 مرحبا العالم',
|
|
65
|
-
EMOJI: '🚀💻🔑',
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
69
|
-
const decrypted = decryptCredentials(encrypted, iv, masterKey);
|
|
70
|
-
|
|
71
|
-
expect(decrypted).toEqual(secrets);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should throw with wrong master key', () => {
|
|
75
|
-
const secrets = { KEY: 'value' };
|
|
76
|
-
const { encrypted, iv } = encryptSecrets(secrets);
|
|
77
|
-
|
|
78
|
-
const wrongKey = '0'.repeat(64);
|
|
79
|
-
|
|
80
|
-
expect(() => decryptCredentials(encrypted, iv, wrongKey)).toThrow();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should throw with wrong IV', () => {
|
|
84
|
-
const secrets = { KEY: 'value' };
|
|
85
|
-
const { encrypted, masterKey } = encryptSecrets(secrets);
|
|
86
|
-
|
|
87
|
-
const wrongIv = '0'.repeat(24);
|
|
88
|
-
|
|
89
|
-
expect(() => decryptCredentials(encrypted, wrongIv, masterKey)).toThrow();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should throw with tampered ciphertext', () => {
|
|
93
|
-
const secrets = { KEY: 'value' };
|
|
94
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
95
|
-
|
|
96
|
-
const tamperedBuffer = Buffer.from(encrypted, 'base64');
|
|
97
|
-
tamperedBuffer[0] = tamperedBuffer[0] ^ 0xff;
|
|
98
|
-
const tampered = tamperedBuffer.toString('base64');
|
|
99
|
-
|
|
100
|
-
expect(() => decryptCredentials(tampered, iv, masterKey)).toThrow();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should handle large number of secrets', () => {
|
|
104
|
-
const secrets: Record<string, string> = {};
|
|
105
|
-
for (let i = 0; i < 50; i++) {
|
|
106
|
-
secrets[`KEY_${i}`] = `value-${i}-${'x'.repeat(50)}`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
110
|
-
const decrypted = decryptCredentials(encrypted, iv, masterKey);
|
|
111
|
-
|
|
112
|
-
expect(decrypted).toEqual(secrets);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should handle secrets with newlines', () => {
|
|
116
|
-
const secrets = {
|
|
117
|
-
PRIVATE_KEY:
|
|
118
|
-
'-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----',
|
|
119
|
-
MULTILINE: 'line1\nline2\nline3',
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const { encrypted, iv, masterKey } = encryptSecrets(secrets);
|
|
123
|
-
const decrypted = decryptCredentials(encrypted, iv, masterKey);
|
|
124
|
-
|
|
125
|
-
expect(decrypted).toEqual(secrets);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('Credentials export', () => {
|
|
130
|
-
it('should return empty object in development mode (no globals defined)', async () => {
|
|
131
|
-
// In test environment, __GKM_ENCRYPTED_CREDENTIALS__ is undefined
|
|
132
|
-
// so Credentials should be an empty object
|
|
133
|
-
const { Credentials } = await import('../credentials');
|
|
134
|
-
expect(Credentials).toEqual({});
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { z } from 'zod/v4';
|
|
3
|
-
import { formatParseError, isDevelopment } from '../formatter';
|
|
4
|
-
|
|
5
|
-
// Helper to create test issues - casts through unknown because we're creating test fixtures
|
|
6
|
-
// that may include properties like 'received' which vary by issue type
|
|
7
|
-
function createIssue(issue: Record<string, unknown>): z.core.$ZodIssue {
|
|
8
|
-
return issue as unknown as z.core.$ZodIssue;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('formatParseError', () => {
|
|
12
|
-
it('should format missing variables', () => {
|
|
13
|
-
const error = new z.ZodError([
|
|
14
|
-
createIssue({
|
|
15
|
-
code: 'invalid_type',
|
|
16
|
-
expected: 'string',
|
|
17
|
-
received: 'undefined',
|
|
18
|
-
path: ['DATABASE_URL'],
|
|
19
|
-
message: 'Environment variable "DATABASE_URL": Required',
|
|
20
|
-
}),
|
|
21
|
-
createIssue({
|
|
22
|
-
code: 'invalid_type',
|
|
23
|
-
expected: 'string',
|
|
24
|
-
received: 'undefined',
|
|
25
|
-
path: ['JWT_SECRET'],
|
|
26
|
-
message: 'Environment variable "JWT_SECRET": Required',
|
|
27
|
-
}),
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
const formatted = formatParseError(error, { colors: false });
|
|
31
|
-
|
|
32
|
-
expect(formatted).toContain('Environment Configuration Failed');
|
|
33
|
-
expect(formatted).toContain('Missing Variables:');
|
|
34
|
-
expect(formatted).toContain('DATABASE_URL');
|
|
35
|
-
expect(formatted).toContain('JWT_SECRET');
|
|
36
|
-
expect(formatted).toContain('Required');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should format invalid values', () => {
|
|
40
|
-
const error = new z.ZodError([
|
|
41
|
-
createIssue({
|
|
42
|
-
code: 'invalid_value',
|
|
43
|
-
values: ['development', 'staging', 'production'],
|
|
44
|
-
received: 'invalid',
|
|
45
|
-
path: ['NODE_ENV'],
|
|
46
|
-
message:
|
|
47
|
-
"Invalid enum value. Expected 'development' | 'staging' | 'production', received 'invalid'",
|
|
48
|
-
}),
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
const formatted = formatParseError(error, { colors: false });
|
|
52
|
-
|
|
53
|
-
expect(formatted).toContain('Environment Configuration Failed');
|
|
54
|
-
expect(formatted).toContain('Invalid Values:');
|
|
55
|
-
expect(formatted).toContain('NODE_ENV');
|
|
56
|
-
expect(formatted).toContain('invalid');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should handle mixed missing and invalid values', () => {
|
|
60
|
-
const error = new z.ZodError([
|
|
61
|
-
createIssue({
|
|
62
|
-
code: 'invalid_type',
|
|
63
|
-
expected: 'string',
|
|
64
|
-
received: 'undefined',
|
|
65
|
-
path: ['DATABASE_URL'],
|
|
66
|
-
message: 'Environment variable "DATABASE_URL": Required',
|
|
67
|
-
}),
|
|
68
|
-
createIssue({
|
|
69
|
-
code: 'invalid_type',
|
|
70
|
-
expected: 'number',
|
|
71
|
-
received: 'string',
|
|
72
|
-
path: ['PORT'],
|
|
73
|
-
message: 'Expected number, received string',
|
|
74
|
-
}),
|
|
75
|
-
]);
|
|
76
|
-
|
|
77
|
-
const formatted = formatParseError(error, { colors: false });
|
|
78
|
-
|
|
79
|
-
expect(formatted).toContain('Missing Variables:');
|
|
80
|
-
expect(formatted).toContain('DATABASE_URL');
|
|
81
|
-
expect(formatted).toContain('Invalid Values:');
|
|
82
|
-
expect(formatted).toContain('PORT');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should clean message prefixes', () => {
|
|
86
|
-
const error = new z.ZodError([
|
|
87
|
-
createIssue({
|
|
88
|
-
code: 'invalid_type',
|
|
89
|
-
expected: 'number',
|
|
90
|
-
received: 'string',
|
|
91
|
-
path: ['PORT'],
|
|
92
|
-
message:
|
|
93
|
-
'Environment variable "PORT": Expected number, received string',
|
|
94
|
-
}),
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
const formatted = formatParseError(error, { colors: false });
|
|
98
|
-
|
|
99
|
-
// Should not duplicate "Environment variable" in output
|
|
100
|
-
expect(formatted).not.toContain(
|
|
101
|
-
'Environment variable "PORT": Environment variable',
|
|
102
|
-
);
|
|
103
|
-
expect(formatted).toContain('Expected number, received string');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should join nested paths with dot', () => {
|
|
107
|
-
const error = new z.ZodError([
|
|
108
|
-
createIssue({
|
|
109
|
-
code: 'invalid_type',
|
|
110
|
-
expected: 'string',
|
|
111
|
-
received: 'undefined',
|
|
112
|
-
path: ['databaseUrl', 'DATABASE_URL'],
|
|
113
|
-
message: 'Environment variable "DATABASE_URL": Required',
|
|
114
|
-
}),
|
|
115
|
-
]);
|
|
116
|
-
|
|
117
|
-
const formatted = formatParseError(error, { colors: false });
|
|
118
|
-
|
|
119
|
-
// Should show the full path joined with '.'
|
|
120
|
-
expect(formatted).toContain('databaseUrl.DATABASE_URL');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('should extract env name from message when path is empty', () => {
|
|
124
|
-
const error = new z.ZodError([
|
|
125
|
-
createIssue({
|
|
126
|
-
code: 'invalid_type',
|
|
127
|
-
expected: 'string',
|
|
128
|
-
received: 'undefined',
|
|
129
|
-
path: [],
|
|
130
|
-
message: 'Environment variable "API_KEY": Required',
|
|
131
|
-
}),
|
|
132
|
-
]);
|
|
133
|
-
|
|
134
|
-
const formatted = formatParseError(error, { colors: false });
|
|
135
|
-
|
|
136
|
-
expect(formatted).toContain('API_KEY');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should handle colors option false', () => {
|
|
140
|
-
const error = new z.ZodError([
|
|
141
|
-
createIssue({
|
|
142
|
-
code: 'invalid_type',
|
|
143
|
-
expected: 'string',
|
|
144
|
-
received: 'undefined',
|
|
145
|
-
path: ['TEST'],
|
|
146
|
-
message: 'Required',
|
|
147
|
-
}),
|
|
148
|
-
]);
|
|
149
|
-
|
|
150
|
-
const formatted = formatParseError(error, { colors: false });
|
|
151
|
-
|
|
152
|
-
// Should not contain ANSI codes
|
|
153
|
-
expect(formatted).not.toContain('\x1b[');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should handle colors option true', () => {
|
|
157
|
-
const error = new z.ZodError([
|
|
158
|
-
createIssue({
|
|
159
|
-
code: 'invalid_type',
|
|
160
|
-
expected: 'string',
|
|
161
|
-
received: 'undefined',
|
|
162
|
-
path: ['TEST'],
|
|
163
|
-
message: 'Required',
|
|
164
|
-
}),
|
|
165
|
-
]);
|
|
166
|
-
|
|
167
|
-
const formatted = formatParseError(error, { colors: true });
|
|
168
|
-
|
|
169
|
-
// Should contain ANSI codes
|
|
170
|
-
expect(formatted).toContain('\x1b[');
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe('isDevelopment', () => {
|
|
175
|
-
const originalEnv = process.env.NODE_ENV;
|
|
176
|
-
|
|
177
|
-
afterEach(() => {
|
|
178
|
-
if (originalEnv === undefined) {
|
|
179
|
-
delete process.env.NODE_ENV;
|
|
180
|
-
} else {
|
|
181
|
-
process.env.NODE_ENV = originalEnv;
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should return true when NODE_ENV is undefined', () => {
|
|
186
|
-
delete process.env.NODE_ENV;
|
|
187
|
-
expect(isDevelopment()).toBe(true);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should return true when NODE_ENV is development', () => {
|
|
191
|
-
process.env.NODE_ENV = 'development';
|
|
192
|
-
expect(isDevelopment()).toBe(true);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('should return true when NODE_ENV is dev', () => {
|
|
196
|
-
process.env.NODE_ENV = 'dev';
|
|
197
|
-
expect(isDevelopment()).toBe(true);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should return true when NODE_ENV is DEVELOPMENT (case insensitive)', () => {
|
|
201
|
-
process.env.NODE_ENV = 'DEVELOPMENT';
|
|
202
|
-
expect(isDevelopment()).toBe(true);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should return false when NODE_ENV is production', () => {
|
|
206
|
-
process.env.NODE_ENV = 'production';
|
|
207
|
-
expect(isDevelopment()).toBe(false);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should return false when NODE_ENV is staging', () => {
|
|
211
|
-
process.env.NODE_ENV = 'staging';
|
|
212
|
-
expect(isDevelopment()).toBe(false);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should return false when NODE_ENV is test', () => {
|
|
216
|
-
process.env.NODE_ENV = 'test';
|
|
217
|
-
expect(isDevelopment()).toBe(false);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
describe('ConfigParser with dev formatting', () => {
|
|
222
|
-
const originalEnv = process.env.NODE_ENV;
|
|
223
|
-
|
|
224
|
-
beforeEach(() => {
|
|
225
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
afterEach(() => {
|
|
229
|
-
vi.restoreAllMocks();
|
|
230
|
-
if (originalEnv === undefined) {
|
|
231
|
-
delete process.env.NODE_ENV;
|
|
232
|
-
} else {
|
|
233
|
-
process.env.NODE_ENV = originalEnv;
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('should log formatted error in development mode', async () => {
|
|
238
|
-
process.env.NODE_ENV = 'development';
|
|
239
|
-
|
|
240
|
-
const { EnvironmentParser } = await import('../EnvironmentParser');
|
|
241
|
-
const parser = new EnvironmentParser({});
|
|
242
|
-
const config = parser.create((get) => ({
|
|
243
|
-
dbUrl: get('DATABASE_URL').string(),
|
|
244
|
-
}));
|
|
245
|
-
|
|
246
|
-
expect(() => config.parse()).toThrow();
|
|
247
|
-
expect(console.error).toHaveBeenCalled();
|
|
248
|
-
|
|
249
|
-
const errorOutput = (console.error as any).mock.calls[0][0];
|
|
250
|
-
expect(errorOutput).toContain('Environment Configuration Failed');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should not log formatted error in production mode', async () => {
|
|
254
|
-
process.env.NODE_ENV = 'production';
|
|
255
|
-
|
|
256
|
-
// Need to reimport to pick up the new NODE_ENV
|
|
257
|
-
vi.resetModules();
|
|
258
|
-
const { EnvironmentParser } = await import('../EnvironmentParser');
|
|
259
|
-
|
|
260
|
-
const parser = new EnvironmentParser({});
|
|
261
|
-
const config = parser.create((get) => ({
|
|
262
|
-
dbUrl: get('DATABASE_URL').string(),
|
|
263
|
-
}));
|
|
264
|
-
|
|
265
|
-
expect(() => config.parse()).toThrow();
|
|
266
|
-
expect(console.error).not.toHaveBeenCalled();
|
|
267
|
-
});
|
|
268
|
-
});
|