env-secrets 0.2.0 → 0.3.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 (60) hide show
  1. package/.devcontainer/devcontainer.json +10 -6
  2. package/.dockerignore +9 -0
  3. package/.eslintignore +4 -2
  4. package/.github/dependabot.yml +4 -0
  5. package/.github/workflows/build-main.yml +6 -2
  6. package/.github/workflows/deploy-docs.yml +50 -0
  7. package/.github/workflows/e2e-tests.yaml +54 -0
  8. package/.github/workflows/lint.yaml +6 -2
  9. package/.github/workflows/release.yml +2 -2
  10. package/.github/workflows/snyk.yaml +5 -1
  11. package/.github/workflows/unittests.yaml +9 -66
  12. package/.lintstagedrc +2 -7
  13. package/.prettierignore +6 -0
  14. package/AGENTS.md +149 -0
  15. package/Dockerfile +14 -0
  16. package/README.md +331 -13
  17. package/__e2e__/README.md +160 -0
  18. package/__e2e__/index.test.ts +334 -32
  19. package/__e2e__/setup.ts +58 -0
  20. package/__e2e__/utils/debug-logger.ts +45 -0
  21. package/__e2e__/utils/test-utils.ts +645 -0
  22. package/__tests__/index.test.ts +266 -9
  23. package/__tests__/vaults/secretsmanager.test.ts +460 -0
  24. package/__tests__/vaults/utils.test.ts +9 -9
  25. package/dist/index.js +36 -10
  26. package/dist/vaults/secretsmanager.js +17 -5
  27. package/dist/vaults/utils.js +2 -2
  28. package/docker-compose.yaml +29 -0
  29. package/docs/AWS.md +257 -0
  30. package/jest.config.js +3 -1
  31. package/jest.e2e.config.js +8 -0
  32. package/package.json +10 -7
  33. package/src/index.ts +44 -10
  34. package/src/vaults/secretsmanager.ts +16 -5
  35. package/src/vaults/utils.ts +6 -4
  36. package/website/docs/advanced-usage.mdx +399 -0
  37. package/website/docs/best-practices.mdx +416 -0
  38. package/website/docs/cli-reference.mdx +204 -0
  39. package/website/docs/examples.mdx +960 -0
  40. package/website/docs/faq.mdx +302 -0
  41. package/website/docs/index.mdx +56 -0
  42. package/website/docs/installation.mdx +30 -0
  43. package/website/docs/overview.mdx +17 -0
  44. package/website/docs/production-deployment.mdx +622 -0
  45. package/website/docs/providers/aws-secrets-manager.mdx +28 -0
  46. package/website/docs/security.mdx +122 -0
  47. package/website/docs/troubleshooting.mdx +236 -0
  48. package/website/docs/tutorials/local-dev/devcontainer-localstack.mdx +31 -0
  49. package/website/docs/tutorials/local-dev/docker-compose.mdx +22 -0
  50. package/website/docs/tutorials/local-dev/nextjs.mdx +18 -0
  51. package/website/docs/tutorials/local-dev/node-python-go.mdx +39 -0
  52. package/website/docs/tutorials/local-dev/quickstart.mdx +23 -0
  53. package/website/docusaurus.config.ts +89 -0
  54. package/website/package.json +21 -0
  55. package/website/sidebars.ts +33 -0
  56. package/website/src/css/custom.css +1 -0
  57. package/website/static/img/env-secrets.png +0 -0
  58. package/website/static/img/favicon.ico +0 -0
  59. package/website/static/img/logo.svg +4 -0
  60. package/website/yarn.lock +8764 -0
@@ -1,37 +1,339 @@
1
+ import {
2
+ LocalStackHelper,
3
+ cli,
4
+ cliWithEnv,
5
+ createTempFile,
6
+ cleanupTempFile,
7
+ createTestProfile,
8
+ restoreTestProfile,
9
+ TestSecret,
10
+ CreatedSecret
11
+ } from './utils/test-utils';
12
+ import { debugLog } from './utils/debug-logger';
13
+ import * as fs from 'fs';
1
14
  import * as path from 'path';
2
- import { exec } from 'child_process';
3
-
4
- type Cli = {
5
- code: number;
6
- error: Error;
7
- stdout: any;
8
- stderr: any;
9
- };
10
-
11
- describe('CLI tests', () => {
12
- test('general help', async () => {
13
- const result = await cli(['-h'], '.');
14
- expect(result.code).toBe(0);
15
+ import * as os from 'os';
16
+
17
+ describe('End-to-End Tests', () => {
18
+ let localStack: LocalStackHelper;
19
+ let awsDir: string | undefined;
20
+
21
+ beforeAll(async () => {
22
+ localStack = new LocalStackHelper();
23
+ await localStack.waitForLocalStack();
24
+
25
+ // Create test AWS profile
26
+ awsDir = createTestProfile();
15
27
  });
16
- test('aws help', async () => {
17
- const result = await cli(['aws -h'], '.');
18
- expect(result.code).toBe(0);
28
+
29
+ afterAll(async () => {
30
+ // Clean up all secrets created during this test run
31
+ await localStack.cleanupRunSecrets();
32
+
33
+ // Restore AWS profile
34
+ restoreTestProfile(awsDir);
35
+ });
36
+
37
+ // Helper function to create a secret for a test
38
+ const createTestSecret = async (
39
+ secret: TestSecret,
40
+ region?: string
41
+ ): Promise<CreatedSecret> => {
42
+ return await localStack.createSecret(secret, region);
43
+ };
44
+
45
+ // Helper function to create LocalStack environment variables
46
+ const getLocalStackEnv = (overrides: Record<string, string> = {}) => ({
47
+ AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
48
+ AWS_ACCESS_KEY_ID: 'test',
49
+ AWS_SECRET_ACCESS_KEY: 'test',
50
+ AWS_DEFAULT_REGION: 'us-east-1',
51
+ ...overrides
19
52
  });
20
- });
21
53
 
22
- function cli(args, cwd): Promise<Cli> {
23
- return new Promise((resolve) => {
24
- exec(
25
- `node ${path.resolve('./dist/index')} ${args.join(' ')}`,
26
- { cwd },
27
- (error, stdout, stderr) => {
28
- resolve({
29
- code: error && error.code ? error.code : 0,
30
- error: error || new Error(),
31
- stdout,
32
- stderr
33
- });
34
- }
35
- );
54
+ describe('CLI Help Commands', () => {
55
+ test('should show general help', async () => {
56
+ const result = await cli(['-h']);
57
+ expect(result.code).toBe(0);
58
+ expect(result.stdout).toContain('env-secrets');
59
+ expect(result.stdout).toContain('pull secrets from vaults');
60
+ });
61
+
62
+ test('should show AWS command help', async () => {
63
+ const result = await cli(['aws', '-h']);
64
+ expect(result.code).toBe(0);
65
+ expect(result.stdout).toContain('get secrets from AWS secrets manager');
66
+ expect(result.stdout).toContain('--secret');
67
+ });
68
+
69
+ test('should show version', async () => {
70
+ const result = await cli(['--version']);
71
+ expect(result.code).toBe(0);
72
+ expect(result.stdout).toMatch(/^\d+\.\d+\.\d+/);
73
+ });
36
74
  });
37
- }
75
+
76
+ describe('AWS Secrets Manager Integration', () => {
77
+ describe('Using Default AWS Credentials', () => {
78
+ test('should retrieve basic JSON secret', async () => {
79
+ const secret = await createTestSecret({
80
+ name: 'test-secret-basic',
81
+ value:
82
+ '{"API_KEY": "secret123", "DATABASE_URL": "postgres://localhost:5432/test"}',
83
+ description: 'Basic test secret with API key and database URL'
84
+ });
85
+
86
+ const result = await cliWithEnv(
87
+ ['aws', '-s', secret.prefixedName],
88
+ getLocalStackEnv()
89
+ );
90
+
91
+ expect(result.code).toBe(0);
92
+ expect(result.stderr).toBe('');
93
+ });
94
+
95
+ test('should retrieve simple string secret', async () => {
96
+ const secret = await createTestSecret({
97
+ name: 'test-secret-simple',
98
+ value: 'simple-string-value',
99
+ description: 'Simple string secret'
100
+ });
101
+
102
+ const result = await cliWithEnv(
103
+ ['aws', '-s', secret.prefixedName],
104
+ getLocalStackEnv()
105
+ );
106
+
107
+ expect(result.code).toBe(0);
108
+ expect(result.stderr).toBe('');
109
+ });
110
+
111
+ test('should retrieve complex JSON secret', async () => {
112
+ const secret = await createTestSecret({
113
+ name: 'test-secret-complex',
114
+ value:
115
+ '{"NESTED": {"KEY": "value"}, "ARRAY": [1, 2, 3], "BOOLEAN": true, "NUMBER": 42}',
116
+ description: 'Complex JSON secret'
117
+ });
118
+
119
+ const result = await cliWithEnv(
120
+ ['aws', '-s', secret.prefixedName],
121
+ getLocalStackEnv()
122
+ );
123
+
124
+ expect(result.code).toBe(0);
125
+ expect(result.stderr).toBe('');
126
+ });
127
+
128
+ test('should retrieve secret with special characters', async () => {
129
+ const secret = await createTestSecret({
130
+ name: 'test-secret-special-chars',
131
+ value:
132
+ '{"PASSWORD": "p@ssw0rd!#$%", "URL": "https://api.example.com/v1?key=value&other=test"}',
133
+ description: 'Secret with special characters'
134
+ });
135
+
136
+ const result = await cliWithEnv(
137
+ ['aws', '-s', secret.prefixedName],
138
+ getLocalStackEnv()
139
+ );
140
+
141
+ expect(result.code).toBe(0);
142
+ expect(result.stderr).toBe('');
143
+ });
144
+
145
+ test('should handle non-existent secret', async () => {
146
+ const result = await cliWithEnv(
147
+ ['aws', '-s', 'non-existent-secret'],
148
+ getLocalStackEnv()
149
+ );
150
+
151
+ expect(result.code).toBe(0);
152
+ expect(result.stderr).toContain('non-existent-secret not found');
153
+ });
154
+
155
+ test('should work with custom region', async () => {
156
+ const secret = await createTestSecret(
157
+ {
158
+ name: `test-secret-region-${Date.now()}`,
159
+ value:
160
+ '{"API_KEY": "secret123", "DATABASE_URL": "postgres://localhost:5432/test"}',
161
+ description: 'Basic test secret for us-west-2'
162
+ },
163
+ 'us-west-2'
164
+ );
165
+
166
+ const result = await cliWithEnv(
167
+ ['aws', '-s', secret.prefixedName, '-r', 'us-west-2'],
168
+ getLocalStackEnv()
169
+ );
170
+
171
+ expect(result.code).toBe(0);
172
+ expect(result.stderr).toBe('');
173
+ });
174
+ });
175
+
176
+ describe('Using AWS Profile', () => {
177
+ test('should retrieve secret using default profile', async () => {
178
+ const secret = await createTestSecret({
179
+ name: 'test-secret-profile',
180
+ value:
181
+ '{"API_KEY": "secret123", "DATABASE_URL": "postgres://localhost:5432/test"}',
182
+ description: 'Secret for profile test'
183
+ });
184
+
185
+ const result = await cliWithEnv(
186
+ ['aws', '-s', secret.prefixedName],
187
+ getLocalStackEnv()
188
+ );
189
+
190
+ expect(result.code).toBe(0);
191
+ expect(result.stderr).toBe('');
192
+ });
193
+
194
+ test('should retrieve secret using custom profile', async () => {
195
+ const secret = await createTestSecret({
196
+ name: 'test-secret-profile-custom',
197
+ value:
198
+ '{"API_KEY": "secret123", "DATABASE_URL": "postgres://localhost:5432/test"}',
199
+ description: 'Secret for custom profile test'
200
+ });
201
+
202
+ const result = await cliWithEnv(
203
+ ['aws', '-s', secret.prefixedName, '-p', 'env-secrets-test'],
204
+ getLocalStackEnv()
205
+ );
206
+
207
+ expect(result.code).toBe(0);
208
+ expect(result.stderr).toBe('');
209
+ });
210
+ });
211
+
212
+ describe('Output to File', () => {
213
+ test('should write secrets to file', async () => {
214
+ debugLog('Starting test...');
215
+
216
+ const secret = await createTestSecret({
217
+ name: `test-secret-file-${Date.now()}`,
218
+ value:
219
+ '{"API_KEY": "secret123", "DATABASE_URL": "postgres://localhost:5432/test"}',
220
+ description: 'Secret for file output test'
221
+ });
222
+ debugLog('Secret created successfully');
223
+
224
+ const tempFile = path.join(
225
+ os.tmpdir(),
226
+ `env-secrets-test-${Date.now()}.env`
227
+ );
228
+
229
+ const result = await cliWithEnv(
230
+ ['aws', '-s', secret.prefixedName, '-o', tempFile],
231
+ getLocalStackEnv()
232
+ );
233
+
234
+ debugLog('CLI result:', {
235
+ code: result.code,
236
+ stdout: result.stdout,
237
+ stderr: result.stderr
238
+ });
239
+ expect(result.code).toBe(0);
240
+ expect(result.stdout).toContain(`Secrets written to ${tempFile}`);
241
+
242
+ // Verify file contents
243
+ const fileContent = fs.readFileSync(tempFile, 'utf8');
244
+ expect(fileContent).toContain('API_KEY=secret123');
245
+ expect(fileContent).toContain(
246
+ 'DATABASE_URL=postgres://localhost:5432/test'
247
+ );
248
+
249
+ cleanupTempFile(tempFile);
250
+ });
251
+
252
+ test('should not overwrite existing file', async () => {
253
+ const tempFile = createTempFile('existing content');
254
+
255
+ try {
256
+ const result = await cliWithEnv(
257
+ ['aws', '-s', 'test-secret-basic', '-o', tempFile],
258
+ getLocalStackEnv()
259
+ );
260
+
261
+ expect(result.code).toBe(1);
262
+ expect(result.stderr).toContain(
263
+ 'already exists and will not be overwritten'
264
+ );
265
+
266
+ // Verify file was not modified
267
+ const fileContent = fs.readFileSync(tempFile, 'utf8');
268
+ expect(fileContent).toBe('existing content');
269
+ } finally {
270
+ cleanupTempFile(tempFile);
271
+ }
272
+ });
273
+ });
274
+
275
+ describe('Program Execution', () => {
276
+ test('should execute program with injected environment variables', async () => {
277
+ const secret = await createTestSecret({
278
+ name: `test-secret-exec-${Date.now()}`,
279
+ value:
280
+ '{"API_KEY": "secret123", "DATABASE_URL": "postgres://localhost:5432/test"}',
281
+ description: 'Secret for program execution test'
282
+ });
283
+
284
+ const result = await cliWithEnv(
285
+ ['aws', '-s', secret.prefixedName, 'echo', '$API_KEY'],
286
+ getLocalStackEnv()
287
+ );
288
+
289
+ expect(result.code).toBe(0);
290
+
291
+ // In test mode, the CLI outputs the environment variables as JSON
292
+ const envVars = JSON.parse(result.stdout.trim());
293
+ expect(envVars.API_KEY).toBe('secret123');
294
+ expect(envVars.DATABASE_URL).toBe('postgres://localhost:5432/test');
295
+ });
296
+
297
+ test('should handle program execution errors gracefully', async () => {
298
+ const secret = await createTestSecret({
299
+ name: `test-secret-error-${Date.now()}`,
300
+ value: '{"API_KEY": "secret123"}',
301
+ description: 'Secret for error handling test'
302
+ });
303
+
304
+ const result = await cliWithEnv(
305
+ ['aws', '-s', secret.prefixedName, 'nonexistent-command'],
306
+ getLocalStackEnv()
307
+ );
308
+
309
+ expect(result.code).toBe(0);
310
+
311
+ // In test mode, the CLI outputs the environment variables as JSON
312
+ const envVars = JSON.parse(result.stdout.trim());
313
+ expect(envVars.API_KEY).toBe('secret123');
314
+ });
315
+ });
316
+
317
+ describe('Error Handling', () => {
318
+ test('should handle invalid AWS credentials', async () => {
319
+ const result = await cliWithEnv(
320
+ ['aws', '-s', 'test-secret-basic'],
321
+ getLocalStackEnv({
322
+ AWS_ACCESS_KEY_ID: 'invalid',
323
+ AWS_SECRET_ACCESS_KEY: 'invalid'
324
+ })
325
+ );
326
+
327
+ // LocalStack accepts any credentials, so this should succeed
328
+ expect(result.code).toBe(0);
329
+ });
330
+
331
+ test('should handle missing secret parameter', async () => {
332
+ const result = await cliWithEnv(['aws'], getLocalStackEnv());
333
+
334
+ expect(result.code).toBe(1);
335
+ expect(result.stderr).toContain('required option');
336
+ });
337
+ });
338
+ });
339
+ });
@@ -0,0 +1,58 @@
1
+ // E2E Test Setup
2
+ // This file runs before all e2e tests
3
+
4
+ import { LocalStackHelper, checkAwslocalInstalled } from './utils/test-utils';
5
+ import { debugLog, debugError } from './utils/debug-logger';
6
+
7
+ // Increase timeout for e2e tests
8
+ jest.setTimeout(30000);
9
+
10
+ // Global setup for e2e tests
11
+ beforeAll(async () => {
12
+ debugLog('Setting up E2E test environment...');
13
+
14
+ // Check if awslocal is installed
15
+ await checkAwslocalInstalled();
16
+
17
+ // Check if LocalStack is available
18
+ const localStack = new LocalStackHelper();
19
+ try {
20
+ await localStack.waitForLocalStack();
21
+ debugLog('LocalStack is ready for E2E tests');
22
+ } catch (error) {
23
+ debugError(
24
+ 'LocalStack is not available, some tests may fail:',
25
+ error.message
26
+ );
27
+ process.exit(1);
28
+ }
29
+ });
30
+
31
+ // Global cleanup
32
+ afterAll(async () => {
33
+ debugLog('Cleaning up E2E test environment...');
34
+ });
35
+
36
+ // Suppress console.log during tests unless DEBUG is set
37
+ // But always show console.error and console.warn for debugging
38
+ const originalConsoleLog = console.log;
39
+ const originalConsoleInfo = console.info;
40
+ const originalConsoleDebug = console.debug;
41
+
42
+ beforeEach(() => {
43
+ if (!process.env.DEBUG) {
44
+ console.log = jest.fn();
45
+ console.info = jest.fn();
46
+ console.debug = jest.fn();
47
+ // Keep console.error and console.warn enabled for debugging
48
+ }
49
+ });
50
+
51
+ afterEach(() => {
52
+ if (!process.env.DEBUG) {
53
+ console.log = originalConsoleLog;
54
+ console.info = originalConsoleInfo;
55
+ console.debug = originalConsoleDebug;
56
+ // console.error and console.warn are already enabled
57
+ }
58
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Debug logging utility for e2e tests
3
+ * Only outputs logs when DEBUG environment variable is set
4
+ */
5
+
6
+ export class DebugLogger {
7
+ private static isDebugEnabled(): boolean {
8
+ return process.env.DEBUG === 'true' || process.env.DEBUG === '1';
9
+ }
10
+
11
+ static log(message: string, ...args: any[]): void {
12
+ if (DebugLogger.isDebugEnabled()) {
13
+ console.log(message, ...args);
14
+ }
15
+ }
16
+
17
+ static error(message: string, ...args: any[]): void {
18
+ // Always show errors for debugging
19
+ console.error(message, ...args);
20
+ }
21
+
22
+ static warn(message: string, ...args: any[]): void {
23
+ // Always show warnings for debugging
24
+ console.warn(message, ...args);
25
+ }
26
+
27
+ static info(message: string, ...args: any[]): void {
28
+ if (DebugLogger.isDebugEnabled()) {
29
+ console.info(message, ...args);
30
+ }
31
+ }
32
+
33
+ static debug(message: string, ...args: any[]): void {
34
+ if (DebugLogger.isDebugEnabled()) {
35
+ console.debug(message, ...args);
36
+ }
37
+ }
38
+ }
39
+
40
+ // Export convenience functions
41
+ export const debugLog = DebugLogger.log;
42
+ export const debugError = DebugLogger.error;
43
+ export const debugWarn = DebugLogger.warn;
44
+ export const debugInfo = DebugLogger.info;
45
+ export const debugDebug = DebugLogger.debug;