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.
- package/.devcontainer/devcontainer.json +10 -6
- package/.dockerignore +9 -0
- package/.eslintignore +4 -2
- package/.github/dependabot.yml +4 -0
- package/.github/workflows/build-main.yml +7 -3
- package/.github/workflows/deploy-docs.yml +50 -0
- package/.github/workflows/e2e-tests.yaml +54 -0
- package/.github/workflows/lint.yaml +7 -3
- package/.github/workflows/release.yml +8 -9
- package/.github/workflows/snyk.yaml +6 -2
- package/.github/workflows/unittests.yaml +9 -66
- package/.lintstagedrc +2 -7
- package/.nvmrc +1 -1
- package/.prettierignore +6 -0
- package/.release-it.json +1 -1
- package/AGENTS.md +149 -0
- package/Dockerfile +14 -0
- package/README.md +332 -14
- package/__e2e__/README.md +160 -0
- package/__e2e__/index.test.ts +334 -32
- package/__e2e__/setup.ts +58 -0
- package/__e2e__/utils/debug-logger.ts +45 -0
- package/__e2e__/utils/test-utils.ts +645 -0
- package/__tests__/index.test.ts +266 -9
- package/__tests__/vaults/secretsmanager.test.ts +460 -0
- package/__tests__/vaults/utils.test.ts +9 -9
- package/dist/index.js +36 -10
- package/dist/vaults/secretsmanager.js +17 -5
- package/dist/vaults/utils.js +2 -2
- package/docker-compose.yaml +29 -0
- package/docs/AWS.md +257 -0
- package/jest.config.js +3 -1
- package/jest.e2e.config.js +8 -0
- package/package.json +19 -12
- package/src/index.ts +44 -10
- package/src/vaults/secretsmanager.ts +16 -5
- package/src/vaults/utils.ts +6 -4
- package/website/docs/advanced-usage.mdx +399 -0
- package/website/docs/best-practices.mdx +416 -0
- package/website/docs/cli-reference.mdx +204 -0
- package/website/docs/examples.mdx +960 -0
- package/website/docs/faq.mdx +302 -0
- package/website/docs/index.mdx +56 -0
- package/website/docs/installation.mdx +30 -0
- package/website/docs/overview.mdx +17 -0
- package/website/docs/production-deployment.mdx +622 -0
- package/website/docs/providers/aws-secrets-manager.mdx +28 -0
- package/website/docs/security.mdx +122 -0
- package/website/docs/troubleshooting.mdx +236 -0
- package/website/docs/tutorials/local-dev/devcontainer-localstack.mdx +31 -0
- package/website/docs/tutorials/local-dev/docker-compose.mdx +22 -0
- package/website/docs/tutorials/local-dev/nextjs.mdx +18 -0
- package/website/docs/tutorials/local-dev/node-python-go.mdx +39 -0
- package/website/docs/tutorials/local-dev/quickstart.mdx +23 -0
- package/website/docusaurus.config.ts +89 -0
- package/website/package.json +21 -0
- package/website/sidebars.ts +33 -0
- package/website/src/css/custom.css +1 -0
- package/website/static/img/env-secrets.png +0 -0
- package/website/static/img/favicon.ico +0 -0
- package/website/static/img/logo.svg +4 -0
- package/website/yarn.lock +8764 -0
package/__tests__/index.test.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|