env-secrets 0.2.0 → 0.3.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 (62) 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 +7 -3
  6. package/.github/workflows/deploy-docs.yml +50 -0
  7. package/.github/workflows/e2e-tests.yaml +54 -0
  8. package/.github/workflows/lint.yaml +7 -3
  9. package/.github/workflows/release.yml +8 -9
  10. package/.github/workflows/snyk.yaml +6 -2
  11. package/.github/workflows/unittests.yaml +9 -66
  12. package/.lintstagedrc +2 -7
  13. package/.nvmrc +1 -1
  14. package/.prettierignore +6 -0
  15. package/.release-it.json +1 -1
  16. package/AGENTS.md +149 -0
  17. package/Dockerfile +14 -0
  18. package/README.md +332 -14
  19. package/__e2e__/README.md +160 -0
  20. package/__e2e__/index.test.ts +334 -32
  21. package/__e2e__/setup.ts +58 -0
  22. package/__e2e__/utils/debug-logger.ts +45 -0
  23. package/__e2e__/utils/test-utils.ts +645 -0
  24. package/__tests__/index.test.ts +266 -9
  25. package/__tests__/vaults/secretsmanager.test.ts +460 -0
  26. package/__tests__/vaults/utils.test.ts +9 -9
  27. package/dist/index.js +36 -10
  28. package/dist/vaults/secretsmanager.js +17 -5
  29. package/dist/vaults/utils.js +2 -2
  30. package/docker-compose.yaml +29 -0
  31. package/docs/AWS.md +257 -0
  32. package/jest.config.js +3 -1
  33. package/jest.e2e.config.js +8 -0
  34. package/package.json +19 -12
  35. package/src/index.ts +44 -10
  36. package/src/vaults/secretsmanager.ts +16 -5
  37. package/src/vaults/utils.ts +6 -4
  38. package/website/docs/advanced-usage.mdx +399 -0
  39. package/website/docs/best-practices.mdx +416 -0
  40. package/website/docs/cli-reference.mdx +204 -0
  41. package/website/docs/examples.mdx +960 -0
  42. package/website/docs/faq.mdx +302 -0
  43. package/website/docs/index.mdx +56 -0
  44. package/website/docs/installation.mdx +30 -0
  45. package/website/docs/overview.mdx +17 -0
  46. package/website/docs/production-deployment.mdx +622 -0
  47. package/website/docs/providers/aws-secrets-manager.mdx +28 -0
  48. package/website/docs/security.mdx +122 -0
  49. package/website/docs/troubleshooting.mdx +236 -0
  50. package/website/docs/tutorials/local-dev/devcontainer-localstack.mdx +31 -0
  51. package/website/docs/tutorials/local-dev/docker-compose.mdx +22 -0
  52. package/website/docs/tutorials/local-dev/nextjs.mdx +18 -0
  53. package/website/docs/tutorials/local-dev/node-python-go.mdx +39 -0
  54. package/website/docs/tutorials/local-dev/quickstart.mdx +23 -0
  55. package/website/docusaurus.config.ts +89 -0
  56. package/website/package.json +21 -0
  57. package/website/sidebars.ts +33 -0
  58. package/website/src/css/custom.css +1 -0
  59. package/website/static/img/env-secrets.png +0 -0
  60. package/website/static/img/favicon.ico +0 -0
  61. package/website/static/img/logo.svg +4 -0
  62. package/website/yarn.lock +8764 -0
@@ -1,14 +1,18 @@
1
- import { Command, Argument } from 'commander';
2
1
  import { spawn } from 'node:child_process';
2
+ import { writeFileSync, existsSync } from 'node:fs';
3
3
  import Debug from 'debug';
4
4
 
5
5
  // Mock external dependencies
6
6
  jest.mock('commander');
7
7
  jest.mock('node:child_process');
8
+ jest.mock('node:fs');
8
9
  jest.mock('debug', () => jest.fn());
9
10
  jest.mock('../src/vaults/secretsmanager', () => ({
10
11
  secretsmanager: jest.fn()
11
12
  }));
13
+ jest.mock('../src/vaults/utils', () => ({
14
+ objectToExport: jest.fn()
15
+ }));
12
16
 
13
17
  // Mock the version import
14
18
  jest.mock('../src/version', () => ({
@@ -17,14 +21,21 @@ jest.mock('../src/version', () => ({
17
21
 
18
22
  // Import after mocking
19
23
  import { secretsmanager } from '../src/vaults/secretsmanager';
24
+ import { objectToExport } from '../src/vaults/utils';
20
25
 
21
26
  // Mock the actual module under test
22
27
  const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
28
+ const mockWriteFileSync = writeFileSync as jest.MockedFunction<
29
+ typeof writeFileSync
30
+ >;
31
+ const mockExistsSync = existsSync as jest.MockedFunction<typeof existsSync>;
23
32
  const mockDebug = Debug as jest.MockedFunction<typeof Debug>;
24
33
  const mockSecretsmanager = secretsmanager as jest.MockedFunction<
25
34
  typeof secretsmanager
26
35
  >;
27
- const mockCommand = Command as jest.MockedClass<typeof Command>;
36
+ const mockObjectToExport = objectToExport as jest.MockedFunction<
37
+ typeof objectToExport
38
+ >;
28
39
 
29
40
  describe('index.ts CLI functionality', () => {
30
41
  beforeEach(() => {
@@ -34,6 +45,7 @@ describe('index.ts CLI functionality', () => {
34
45
  process.env = { ...process.env };
35
46
 
36
47
  // Setup mock debug
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
49
  const mockDebugInstance = jest.fn() as any;
38
50
  mockDebug.mockReturnValue(mockDebugInstance);
39
51
  });
@@ -84,10 +96,12 @@ describe('index.ts CLI functionality', () => {
84
96
  const mockEnv = { SECRET_KEY: 'secret_value' };
85
97
  mockSecretsmanager.mockResolvedValue(mockEnv);
86
98
 
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
100
  const mockChildProcess = {
88
101
  stdio: 'inherit'
89
- };
90
- mockSpawn.mockReturnValue(mockChildProcess as any);
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ } as any;
104
+ mockSpawn.mockReturnValue(mockChildProcess);
91
105
 
92
106
  const program = ['node', 'script.js', 'arg1', 'arg2'];
93
107
  const options = { secret: 'my-secret' };
@@ -161,10 +175,12 @@ describe('index.ts CLI functionality', () => {
161
175
  const mockEnv = { SECRET_KEY: 'secret_value' };
162
176
  mockSecretsmanager.mockResolvedValue(mockEnv);
163
177
 
178
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
179
  const mockChildProcess = {
165
180
  stdio: 'inherit'
166
- };
167
- mockSpawn.mockReturnValue(mockChildProcess as any);
181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ } as any;
183
+ mockSpawn.mockReturnValue(mockChildProcess);
168
184
 
169
185
  const program = ['echo'];
170
186
  const options = { secret: 'my-secret' };
@@ -262,11 +278,12 @@ describe('index.ts CLI functionality', () => {
262
278
 
263
279
  describe('Debug logging', () => {
264
280
  it('should create debug instance with correct namespace', () => {
265
- const debugInstance = mockDebug('env-secrets');
281
+ mockDebug('env-secrets');
266
282
  expect(mockDebug).toHaveBeenCalledWith('env-secrets');
267
283
  });
268
284
 
269
285
  it('should log environment variables when debug is enabled', async () => {
286
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
270
287
  const mockDebugInstance = jest.fn() as any;
271
288
  mockDebug.mockReturnValue(mockDebugInstance);
272
289
 
@@ -288,16 +305,19 @@ describe('index.ts CLI functionality', () => {
288
305
  });
289
306
 
290
307
  it('should log program execution when program is provided', async () => {
308
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
291
309
  const mockDebugInstance = jest.fn() as any;
292
310
  mockDebug.mockReturnValue(mockDebugInstance);
293
311
 
294
312
  const mockEnv = { SECRET_KEY: 'secret_value' };
295
313
  mockSecretsmanager.mockResolvedValue(mockEnv);
296
314
 
315
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
297
316
  const mockChildProcess = {
298
317
  stdio: 'inherit'
299
- };
300
- mockSpawn.mockReturnValue(mockChildProcess as any);
318
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
+ } as any;
320
+ mockSpawn.mockReturnValue(mockChildProcess);
301
321
 
302
322
  const program = ['node', 'script.js', 'arg1'];
303
323
 
@@ -319,4 +339,241 @@ describe('index.ts CLI functionality', () => {
319
339
  expect(mockDebugInstance).toHaveBeenCalledWith('node script.js arg1');
320
340
  });
321
341
  });
342
+
343
+ describe('File output functionality', () => {
344
+ beforeEach(() => {
345
+ // Reset console methods
346
+ jest.spyOn(console, 'log').mockImplementation(() => undefined);
347
+ jest.spyOn(console, 'error').mockImplementation(() => undefined);
348
+ jest.spyOn(process, 'exit').mockImplementation(() => {
349
+ throw new Error('process.exit called');
350
+ });
351
+ });
352
+
353
+ afterEach(() => {
354
+ jest.restoreAllMocks();
355
+ });
356
+
357
+ it('should write secrets to file when output option is provided', async () => {
358
+ const mockSecrets = { SECRET_KEY: 'secret_value', API_KEY: 'api_value' };
359
+ const mockEnvContent =
360
+ 'export SECRET_KEY=secret_value\nexport API_KEY=api_value\n';
361
+
362
+ mockSecretsmanager.mockResolvedValue(mockSecrets);
363
+ mockObjectToExport.mockReturnValue(mockEnvContent);
364
+ mockExistsSync.mockReturnValue(false);
365
+
366
+ const options: { secret: string; output: string } = {
367
+ secret: 'my-secret',
368
+ output: '/tmp/secrets.env'
369
+ };
370
+
371
+ // Simulate the action logic
372
+ const secrets = await mockSecretsmanager(options);
373
+
374
+ if (options.output) {
375
+ if (mockExistsSync(options.output)) {
376
+ // eslint-disable-next-line no-console
377
+ console.error(
378
+ `Error: File ${options.output} already exists and will not be overwritten`
379
+ );
380
+ process.exit(1);
381
+ }
382
+
383
+ const envContent = mockObjectToExport(secrets);
384
+ mockWriteFileSync(options.output, envContent, { mode: 0o400 });
385
+ // eslint-disable-next-line no-console
386
+ console.log(`Secrets written to ${options.output}`);
387
+ }
388
+
389
+ expect(mockSecretsmanager).toHaveBeenCalledWith(options);
390
+ expect(mockObjectToExport).toHaveBeenCalledWith(mockSecrets);
391
+ expect(mockWriteFileSync).toHaveBeenCalledWith(
392
+ '/tmp/secrets.env',
393
+ mockEnvContent,
394
+ { mode: 0o400 }
395
+ );
396
+ // eslint-disable-next-line no-console
397
+ expect(console.log).toHaveBeenCalledWith(
398
+ 'Secrets written to /tmp/secrets.env'
399
+ );
400
+ });
401
+
402
+ it('should not overwrite existing file', async () => {
403
+ const mockSecrets = { SECRET_KEY: 'secret_value' };
404
+
405
+ mockSecretsmanager.mockResolvedValue(mockSecrets);
406
+ mockExistsSync.mockReturnValue(true);
407
+
408
+ const options: { secret: string; output: string } = {
409
+ secret: 'my-secret',
410
+ output: '/tmp/existing.env'
411
+ };
412
+
413
+ // Simulate the action logic
414
+ await mockSecretsmanager(options);
415
+
416
+ // Test the file existence check and error handling
417
+ if (options.output) {
418
+ if (mockExistsSync(options.output)) {
419
+ // eslint-disable-next-line no-console
420
+ console.error(
421
+ `Error: File ${options.output} already exists and will not be overwritten`
422
+ );
423
+ // In the actual implementation, this would call process.exit(1)
424
+ }
425
+ }
426
+
427
+ expect(mockExistsSync).toHaveBeenCalledWith('/tmp/existing.env');
428
+ // eslint-disable-next-line no-console
429
+ expect(console.error).toHaveBeenCalledWith(
430
+ 'Error: File /tmp/existing.env already exists and will not be overwritten'
431
+ );
432
+ expect(mockWriteFileSync).not.toHaveBeenCalled();
433
+ });
434
+
435
+ it('should use original behavior when no output option is provided', async () => {
436
+ const mockSecrets = { SECRET_KEY: 'secret_value' };
437
+ mockSecretsmanager.mockResolvedValue(mockSecrets);
438
+
439
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
440
+ const mockChildProcess = {
441
+ stdio: 'inherit'
442
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
443
+ } as any;
444
+ mockSpawn.mockReturnValue(mockChildProcess);
445
+
446
+ const options: { secret: string; output?: string } = {
447
+ secret: 'my-secret'
448
+ // No output option
449
+ };
450
+
451
+ const program = ['echo', 'hello'];
452
+
453
+ // Simulate the action logic
454
+ const secrets = await mockSecretsmanager(options);
455
+
456
+ if (options.output) {
457
+ // This branch should not be taken
458
+ if (mockExistsSync(options.output)) {
459
+ // eslint-disable-next-line no-console
460
+ console.error(
461
+ `Error: File ${options.output} already exists and will not be overwritten`
462
+ );
463
+ process.exit(1);
464
+ }
465
+
466
+ const envContent = mockObjectToExport(secrets);
467
+ mockWriteFileSync(options.output, envContent, { mode: 0o400 });
468
+ // eslint-disable-next-line no-console
469
+ console.log(`Secrets written to ${options.output}`);
470
+ } else {
471
+ // Original behavior: merge secrets into environment and run program
472
+ const env = Object.assign({}, process.env, secrets);
473
+
474
+ if (program && program.length > 0) {
475
+ mockSpawn(program[0], program.slice(1), {
476
+ stdio: 'inherit',
477
+ shell: true,
478
+ env
479
+ });
480
+ }
481
+ }
482
+
483
+ expect(mockSecretsmanager).toHaveBeenCalledWith(options);
484
+ expect(mockSpawn).toHaveBeenCalledWith('echo', ['hello'], {
485
+ stdio: 'inherit',
486
+ shell: true,
487
+ env: expect.objectContaining({
488
+ SECRET_KEY: 'secret_value'
489
+ })
490
+ });
491
+ expect(mockWriteFileSync).not.toHaveBeenCalled();
492
+ });
493
+
494
+ it('should handle empty secrets object in file output', async () => {
495
+ const mockSecrets = {};
496
+ const mockEnvContent = '';
497
+
498
+ mockSecretsmanager.mockResolvedValue(mockSecrets);
499
+ mockObjectToExport.mockReturnValue(mockEnvContent);
500
+ mockExistsSync.mockReturnValue(false);
501
+
502
+ const options: { secret: string; output: string } = {
503
+ secret: 'my-secret',
504
+ output: '/tmp/empty.env'
505
+ };
506
+
507
+ // Simulate the action logic
508
+ const secrets = await mockSecretsmanager(options);
509
+
510
+ if (options.output) {
511
+ if (mockExistsSync(options.output)) {
512
+ // eslint-disable-next-line no-console
513
+ console.error(
514
+ `Error: File ${options.output} already exists and will not be overwritten`
515
+ );
516
+ process.exit(1);
517
+ }
518
+
519
+ const envContent = mockObjectToExport(secrets);
520
+ mockWriteFileSync(options.output, envContent, { mode: 0o400 });
521
+ // eslint-disable-next-line no-console
522
+ console.log(`Secrets written to ${options.output}`);
523
+ }
524
+
525
+ expect(mockObjectToExport).toHaveBeenCalledWith({});
526
+ expect(mockWriteFileSync).toHaveBeenCalledWith('/tmp/empty.env', '', {
527
+ mode: 0o400
528
+ });
529
+ });
530
+
531
+ it('should not run program when output option is provided', async () => {
532
+ const mockSecrets = { SECRET_KEY: 'secret_value' };
533
+ const mockEnvContent = 'export SECRET_KEY=secret_value\n';
534
+
535
+ mockSecretsmanager.mockResolvedValue(mockSecrets);
536
+ mockObjectToExport.mockReturnValue(mockEnvContent);
537
+ mockExistsSync.mockReturnValue(false);
538
+
539
+ const options: { secret: string; output: string } = {
540
+ secret: 'my-secret',
541
+ output: '/tmp/secrets.env'
542
+ };
543
+
544
+ const program = ['echo', 'hello'];
545
+
546
+ // Simulate the action logic
547
+ const secrets = await mockSecretsmanager(options);
548
+
549
+ if (options.output) {
550
+ if (mockExistsSync(options.output)) {
551
+ // eslint-disable-next-line no-console
552
+ console.error(
553
+ `Error: File ${options.output} already exists and will not be overwritten`
554
+ );
555
+ process.exit(1);
556
+ }
557
+
558
+ const envContent = mockObjectToExport(secrets);
559
+ mockWriteFileSync(options.output, envContent, { mode: 0o400 });
560
+ // eslint-disable-next-line no-console
561
+ console.log(`Secrets written to ${options.output}`);
562
+ } else {
563
+ // This branch should not be taken
564
+ const env = Object.assign({}, process.env, secrets);
565
+
566
+ if (program && program.length > 0) {
567
+ mockSpawn(program[0], program.slice(1), {
568
+ stdio: 'inherit',
569
+ shell: true,
570
+ env
571
+ });
572
+ }
573
+ }
574
+
575
+ expect(mockWriteFileSync).toHaveBeenCalled();
576
+ expect(mockSpawn).not.toHaveBeenCalled();
577
+ });
578
+ });
322
579
  });