env-secrets 0.4.0 → 0.5.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/AGENTS.md +3 -0
- package/README.md +40 -1
- package/__e2e__/index.test.ts +189 -1
- package/__tests__/cli/helpers.test.ts +89 -1
- package/__tests__/vaults/secretsmanager-admin.test.ts +43 -0
- package/dist/cli/helpers.js +66 -4
- package/dist/index.js +245 -10
- package/dist/vaults/secretsmanager-admin.js +34 -1
- package/docs/AWS.md +55 -3
- package/package.json +4 -4
- package/src/cli/helpers.ts +98 -3
- package/src/index.ts +313 -5
- package/src/vaults/secretsmanager-admin.ts +45 -0
- package/website/docs/cli-reference.mdx +36 -2
- package/website/docs/providers/aws-secrets-manager.mdx +28 -1
package/AGENTS.md
CHANGED
|
@@ -133,6 +133,9 @@ yarn test:unit:coverage # Run tests with coverage
|
|
|
133
133
|
4. Ensure all CI checks pass
|
|
134
134
|
5. Submit a pull request with a clear description
|
|
135
135
|
6. Always request a GitHub Copilot review on every new pull request
|
|
136
|
+
7. After requesting Copilot review, wait 5 minutes and check for review comments
|
|
137
|
+
8. If no Copilot review is present yet, wait another 5 minutes and check again
|
|
138
|
+
9. Create a plan to address Copilot feedback, but evaluate each suggestion critically and do not accept recommendations blindly
|
|
136
139
|
|
|
137
140
|
## Development Environment
|
|
138
141
|
|
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ env-secrets aws -s my-app-secrets -r us-east-1 -- node app.js
|
|
|
103
103
|
- `-o, --output <file>` (optional): Output secrets to a file instead of injecting into environment variables. File will be created with 0400 permissions and will not overwrite existing files
|
|
104
104
|
- `-- <program-to-run>`: The program to run with the injected environment variables (only used when `-o` is not specified)
|
|
105
105
|
|
|
106
|
-
For `aws secret` management subcommands (`create`, `update`, `list`, `get`, `delete`), use:
|
|
106
|
+
For `aws secret` management subcommands (`create`, `update`, `append`, `remove`, `upsert`/`import`, `list`, `get`, `delete`), use:
|
|
107
107
|
|
|
108
108
|
- `-r, --region <region>` to target a specific region
|
|
109
109
|
- `-p, --profile <profile>` to select credentials profile
|
|
@@ -111,6 +111,9 @@ For `aws secret` management subcommands (`create`, `update`, `list`, `get`, `del
|
|
|
111
111
|
|
|
112
112
|
These options are honored consistently on `aws secret` subcommands.
|
|
113
113
|
|
|
114
|
+
`env-secrets aws -s` is for fetching/injecting secret values into a child process.
|
|
115
|
+
`env-secrets aws secret ...` is for lifecycle management commands (`create`, `update`, `append`, `remove`, `upsert`/`import`, `list`, `get`, `delete`).
|
|
116
|
+
|
|
114
117
|
#### Examples
|
|
115
118
|
|
|
116
119
|
1. **Create a secret using AWS CLI:**
|
|
@@ -217,6 +220,42 @@ env-secrets aws -s my-secret -r us-east-1 -o secrets.env
|
|
|
217
220
|
# Error: File secrets.env already exists and will not be overwritten
|
|
218
221
|
```
|
|
219
222
|
|
|
223
|
+
10. **Load secrets into your current shell session:**
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Write export statements to a file
|
|
227
|
+
env-secrets aws -s my-secret -r us-east-1 -o secrets.env
|
|
228
|
+
|
|
229
|
+
# Load into current shell
|
|
230
|
+
source secrets.env
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Note: `env-secrets aws -s ... -- <command>` injects secrets into the spawned child process only.
|
|
234
|
+
To affect your current shell, use file output and `source` it.
|
|
235
|
+
|
|
236
|
+
11. **Upsert secrets from a local env file:**
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Supported line formats:
|
|
240
|
+
# export NAME=secret1
|
|
241
|
+
# NAME=secret1
|
|
242
|
+
env-secrets aws secret upsert --file .env --name app/dev --output json
|
|
243
|
+
|
|
244
|
+
# Creates/updates a single secret named app/dev
|
|
245
|
+
# with SecretString like:
|
|
246
|
+
# {"NAME":"secret1"}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
12. **Append/remove keys on an existing JSON secret:**
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Add or overwrite one key
|
|
253
|
+
env-secrets aws secret append -n app/dev --key JIRA_EMAIL_TOKEN -v blah --output json
|
|
254
|
+
|
|
255
|
+
# Remove one or more keys
|
|
256
|
+
env-secrets aws secret remove -n app/dev --key API_KEY --key OLD_TOKEN --output json
|
|
257
|
+
```
|
|
258
|
+
|
|
220
259
|
## Security Considerations
|
|
221
260
|
|
|
222
261
|
- 🔐 **Credential Management**: The tool respects AWS credential precedence (environment variables, IAM roles, profiles)
|
package/__e2e__/index.test.ts
CHANGED
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
createTestProfile,
|
|
9
9
|
restoreTestProfile,
|
|
10
10
|
TestSecret,
|
|
11
|
-
CreatedSecret
|
|
11
|
+
CreatedSecret,
|
|
12
|
+
execAwslocalCommand
|
|
12
13
|
} from './utils/test-utils';
|
|
13
14
|
import { debugLog } from './utils/debug-logger';
|
|
14
15
|
import * as fs from 'fs';
|
|
@@ -417,6 +418,97 @@ describe('End-to-End Tests', () => {
|
|
|
417
418
|
expect(deleteResult.code).toBe(0);
|
|
418
419
|
});
|
|
419
420
|
|
|
421
|
+
test('should append and remove keys on a JSON secret', async () => {
|
|
422
|
+
const secretName = `managed-secret-append-remove-${Date.now()}`;
|
|
423
|
+
const tempFile = path.join(
|
|
424
|
+
os.tmpdir(),
|
|
425
|
+
`env-secrets-append-remove-${Date.now()}.env`
|
|
426
|
+
);
|
|
427
|
+
fs.writeFileSync(tempFile, 'API_KEY=first');
|
|
428
|
+
|
|
429
|
+
const createResult = await cliWithEnv(
|
|
430
|
+
[
|
|
431
|
+
'aws',
|
|
432
|
+
'secret',
|
|
433
|
+
'upsert',
|
|
434
|
+
'--file',
|
|
435
|
+
tempFile,
|
|
436
|
+
'--name',
|
|
437
|
+
secretName,
|
|
438
|
+
'--output',
|
|
439
|
+
'json'
|
|
440
|
+
],
|
|
441
|
+
getLocalStackEnv()
|
|
442
|
+
);
|
|
443
|
+
expect(createResult.code).toBe(0);
|
|
444
|
+
|
|
445
|
+
const appendResult = await cliWithEnv(
|
|
446
|
+
[
|
|
447
|
+
'aws',
|
|
448
|
+
'secret',
|
|
449
|
+
'append',
|
|
450
|
+
'-n',
|
|
451
|
+
secretName,
|
|
452
|
+
'--key',
|
|
453
|
+
'JIRA_EMAIL_TOKEN',
|
|
454
|
+
'-v',
|
|
455
|
+
'blah',
|
|
456
|
+
'--output',
|
|
457
|
+
'json'
|
|
458
|
+
],
|
|
459
|
+
getLocalStackEnv()
|
|
460
|
+
);
|
|
461
|
+
expect(appendResult.code).toBe(0);
|
|
462
|
+
|
|
463
|
+
const afterAppend = await execAwslocalCommand(
|
|
464
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
465
|
+
getLocalStackEnv()
|
|
466
|
+
);
|
|
467
|
+
expect(JSON.parse(afterAppend.stdout.trim())).toEqual({
|
|
468
|
+
API_KEY: 'first',
|
|
469
|
+
JIRA_EMAIL_TOKEN: 'blah'
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const removeResult = await cliWithEnv(
|
|
473
|
+
[
|
|
474
|
+
'aws',
|
|
475
|
+
'secret',
|
|
476
|
+
'remove',
|
|
477
|
+
'-n',
|
|
478
|
+
secretName,
|
|
479
|
+
'--key',
|
|
480
|
+
'API_KEY',
|
|
481
|
+
'--output',
|
|
482
|
+
'json'
|
|
483
|
+
],
|
|
484
|
+
getLocalStackEnv()
|
|
485
|
+
);
|
|
486
|
+
expect(removeResult.code).toBe(0);
|
|
487
|
+
|
|
488
|
+
const afterRemove = await execAwslocalCommand(
|
|
489
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
490
|
+
getLocalStackEnv()
|
|
491
|
+
);
|
|
492
|
+
expect(JSON.parse(afterRemove.stdout.trim())).toEqual({
|
|
493
|
+
JIRA_EMAIL_TOKEN: 'blah'
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const deleteResult = await cliWithEnv(
|
|
497
|
+
[
|
|
498
|
+
'aws',
|
|
499
|
+
'secret',
|
|
500
|
+
'delete',
|
|
501
|
+
'-n',
|
|
502
|
+
secretName,
|
|
503
|
+
'--force-delete-without-recovery',
|
|
504
|
+
'--yes'
|
|
505
|
+
],
|
|
506
|
+
getLocalStackEnv()
|
|
507
|
+
);
|
|
508
|
+
expect(deleteResult.code).toBe(0);
|
|
509
|
+
cleanupTempFile(tempFile);
|
|
510
|
+
});
|
|
511
|
+
|
|
420
512
|
test('should require confirmation for delete', async () => {
|
|
421
513
|
const secret = await createTestSecret({
|
|
422
514
|
name: `managed-secret-confirm-${Date.now()}`,
|
|
@@ -485,6 +577,102 @@ describe('End-to-End Tests', () => {
|
|
|
485
577
|
}>;
|
|
486
578
|
expect(eastRows).toEqual([]);
|
|
487
579
|
});
|
|
580
|
+
|
|
581
|
+
test('should upsert secrets from env file', async () => {
|
|
582
|
+
const secretName = `e2e-upsert-${Date.now()}`;
|
|
583
|
+
const tempFile = path.join(
|
|
584
|
+
os.tmpdir(),
|
|
585
|
+
`env-secrets-upsert-${Date.now()}.env`
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
fs.writeFileSync(
|
|
589
|
+
tempFile,
|
|
590
|
+
['# sample', 'export API_KEY=first', 'DB_URL = postgres://one'].join(
|
|
591
|
+
'\n'
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const firstRun = await cliWithEnv(
|
|
596
|
+
[
|
|
597
|
+
'aws',
|
|
598
|
+
'secret',
|
|
599
|
+
'upsert',
|
|
600
|
+
'--file',
|
|
601
|
+
tempFile,
|
|
602
|
+
'--name',
|
|
603
|
+
secretName,
|
|
604
|
+
'--output',
|
|
605
|
+
'json'
|
|
606
|
+
],
|
|
607
|
+
getLocalStackEnv()
|
|
608
|
+
);
|
|
609
|
+
expect(firstRun.code).toBe(0);
|
|
610
|
+
const firstJson = JSON.parse(firstRun.stdout) as {
|
|
611
|
+
summary: { created: number; updated: number; skipped: number };
|
|
612
|
+
};
|
|
613
|
+
expect(firstJson.summary.created).toBe(1);
|
|
614
|
+
expect(firstJson.summary.updated).toBe(0);
|
|
615
|
+
|
|
616
|
+
const firstSecret = await execAwslocalCommand(
|
|
617
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
618
|
+
getLocalStackEnv()
|
|
619
|
+
);
|
|
620
|
+
expect(JSON.parse(firstSecret.stdout.trim())).toEqual({
|
|
621
|
+
API_KEY: 'first',
|
|
622
|
+
DB_URL: 'postgres://one'
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
fs.writeFileSync(
|
|
626
|
+
tempFile,
|
|
627
|
+
['export API_KEY=second', 'DB_URL=postgres://two'].join('\n')
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
const secondRun = await cliWithEnv(
|
|
631
|
+
[
|
|
632
|
+
'aws',
|
|
633
|
+
'secret',
|
|
634
|
+
'import',
|
|
635
|
+
'--file',
|
|
636
|
+
tempFile,
|
|
637
|
+
'--name',
|
|
638
|
+
secretName,
|
|
639
|
+
'--output',
|
|
640
|
+
'json'
|
|
641
|
+
],
|
|
642
|
+
getLocalStackEnv()
|
|
643
|
+
);
|
|
644
|
+
expect(secondRun.code).toBe(0);
|
|
645
|
+
const secondJson = JSON.parse(secondRun.stdout) as {
|
|
646
|
+
summary: { created: number; updated: number; skipped: number };
|
|
647
|
+
};
|
|
648
|
+
expect(secondJson.summary.created).toBe(0);
|
|
649
|
+
expect(secondJson.summary.updated).toBe(1);
|
|
650
|
+
expect(secondJson.summary.skipped).toBe(0);
|
|
651
|
+
|
|
652
|
+
const secondSecret = await execAwslocalCommand(
|
|
653
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
654
|
+
getLocalStackEnv()
|
|
655
|
+
);
|
|
656
|
+
expect(JSON.parse(secondSecret.stdout.trim())).toEqual({
|
|
657
|
+
API_KEY: 'second',
|
|
658
|
+
DB_URL: 'postgres://two'
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const deleteResult = await cliWithEnv(
|
|
662
|
+
[
|
|
663
|
+
'aws',
|
|
664
|
+
'secret',
|
|
665
|
+
'delete',
|
|
666
|
+
'-n',
|
|
667
|
+
secretName,
|
|
668
|
+
'--force-delete-without-recovery',
|
|
669
|
+
'--yes'
|
|
670
|
+
],
|
|
671
|
+
getLocalStackEnv()
|
|
672
|
+
);
|
|
673
|
+
expect(deleteResult.code).toBe(0);
|
|
674
|
+
cleanupTempFile(tempFile);
|
|
675
|
+
});
|
|
488
676
|
});
|
|
489
677
|
});
|
|
490
678
|
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { PassThrough } from 'node:stream';
|
|
2
5
|
|
|
3
6
|
import {
|
|
4
7
|
asOutputFormat,
|
|
8
|
+
parseEnvSecrets,
|
|
5
9
|
parseRecoveryDays,
|
|
6
10
|
printData,
|
|
7
11
|
readStdin,
|
|
@@ -73,7 +77,29 @@ describe('cli/helpers', () => {
|
|
|
73
77
|
|
|
74
78
|
it('rejects when both --value and --value-stdin are used', async () => {
|
|
75
79
|
await expect(resolveSecretValue('inline', true)).rejects.toThrow(
|
|
76
|
-
'Use
|
|
80
|
+
'Use only one secret value source: --value, --value-stdin, or --file.'
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects when --file and --value-stdin are used together', async () => {
|
|
85
|
+
await expect(
|
|
86
|
+
resolveSecretValue(undefined, true, './secret.txt')
|
|
87
|
+
).rejects.toThrow(
|
|
88
|
+
'Use only one secret value source: --value, --value-stdin, or --file.'
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('rejects when --value and --file are used together', async () => {
|
|
93
|
+
await expect(
|
|
94
|
+
resolveSecretValue('inline', false, './secret.txt')
|
|
95
|
+
).rejects.toThrow(
|
|
96
|
+
'Use only one secret value source: --value, --value-stdin, or --file.'
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('treats explicit empty --value as provided for mutual exclusion', async () => {
|
|
101
|
+
await expect(resolveSecretValue('', false, './secret.txt')).rejects.toThrow(
|
|
102
|
+
'Use only one secret value source: --value, --value-stdin, or --file.'
|
|
77
103
|
);
|
|
78
104
|
});
|
|
79
105
|
|
|
@@ -94,6 +120,68 @@ describe('cli/helpers', () => {
|
|
|
94
120
|
});
|
|
95
121
|
});
|
|
96
122
|
|
|
123
|
+
it('reads secret value from file and strips one trailing newline', async () => {
|
|
124
|
+
const dir = mkdtempSync(join(tmpdir(), 'env-secrets-test-'));
|
|
125
|
+
const file = join(dir, 'secret.txt');
|
|
126
|
+
writeFileSync(file, 'file-secret\n');
|
|
127
|
+
|
|
128
|
+
await expect(resolveSecretValue(undefined, false, file)).resolves.toBe(
|
|
129
|
+
'file-secret'
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('parses env secrets from KEY=value and export KEY=value formats', () => {
|
|
136
|
+
const parsed = parseEnvSecrets(
|
|
137
|
+
[
|
|
138
|
+
'# comment',
|
|
139
|
+
'export API_KEY=secret1',
|
|
140
|
+
'DATABASE_URL=postgres://db',
|
|
141
|
+
''
|
|
142
|
+
].join('\n')
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(parsed.entries).toEqual([
|
|
146
|
+
{ key: 'API_KEY', value: 'secret1', line: 2 },
|
|
147
|
+
{ key: 'DATABASE_URL', value: 'postgres://db', line: 3 }
|
|
148
|
+
]);
|
|
149
|
+
expect(parsed.skipped).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('handles whitespace around equals in env file', () => {
|
|
153
|
+
const parsed = parseEnvSecrets(' export NAME = secret1 ');
|
|
154
|
+
|
|
155
|
+
expect(parsed.entries).toEqual([
|
|
156
|
+
{ key: 'NAME', value: 'secret1', line: 1 }
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('skips duplicate keys when parsing env file', () => {
|
|
161
|
+
const parsed = parseEnvSecrets(['A=1', 'A=2'].join('\n'));
|
|
162
|
+
|
|
163
|
+
expect(parsed.entries).toEqual([{ key: 'A', value: '1', line: 1 }]);
|
|
164
|
+
expect(parsed.skipped).toEqual([
|
|
165
|
+
{ key: 'A', line: 2, reason: 'duplicate key' }
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('throws clear error for malformed env lines', () => {
|
|
170
|
+
expect(() => parseEnvSecrets('NOT_A_VALID_LINE')).toThrow(
|
|
171
|
+
'Malformed env line 1'
|
|
172
|
+
);
|
|
173
|
+
expect(() => parseEnvSecrets('BAD=secret')).not.toThrow();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('does not include raw line content in malformed env errors', () => {
|
|
177
|
+
expect(() => parseEnvSecrets('export SECRET_ONLY')).toThrow(
|
|
178
|
+
'Expected KEY=value or export KEY=value.'
|
|
179
|
+
);
|
|
180
|
+
expect(() => parseEnvSecrets('export SECRET_ONLY')).not.toThrow(
|
|
181
|
+
/SECRET_ONLY/
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
97
185
|
it('prefers explicit aws scope options over global options', () => {
|
|
98
186
|
const command = {
|
|
99
187
|
optsWithGlobals: () => ({
|
|
@@ -12,6 +12,7 @@ jest.mock('@aws-sdk/client-secrets-manager', () => {
|
|
|
12
12
|
})),
|
|
13
13
|
CreateSecretCommand: jest.fn().mockImplementation((input) => ({ input })),
|
|
14
14
|
UpdateSecretCommand: jest.fn().mockImplementation((input) => ({ input })),
|
|
15
|
+
GetSecretValueCommand: jest.fn().mockImplementation((input) => ({ input })),
|
|
15
16
|
ListSecretsCommand: jest.fn().mockImplementation((input) => ({ input })),
|
|
16
17
|
DescribeSecretCommand: jest.fn().mockImplementation((input) => ({ input })),
|
|
17
18
|
DeleteSecretCommand: jest.fn().mockImplementation((input) => ({ input }))
|
|
@@ -37,6 +38,8 @@ import {
|
|
|
37
38
|
listSecrets,
|
|
38
39
|
getSecretMetadata,
|
|
39
40
|
deleteSecret,
|
|
41
|
+
secretExists,
|
|
42
|
+
getSecretString,
|
|
40
43
|
validateSecretName
|
|
41
44
|
} from '../../src/vaults/secretsmanager-admin';
|
|
42
45
|
|
|
@@ -278,6 +281,46 @@ describe('secretsmanager-admin', () => {
|
|
|
278
281
|
expect(Object.keys(result)).not.toContain('secretString');
|
|
279
282
|
});
|
|
280
283
|
|
|
284
|
+
it('returns true when secret exists', async () => {
|
|
285
|
+
mockSecretsManagerSend.mockResolvedValueOnce({
|
|
286
|
+
Name: 'app/existing'
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await expect(
|
|
290
|
+
secretExists({ name: 'app/existing', region: 'us-east-1' })
|
|
291
|
+
).resolves.toBe(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('returns false when secret does not exist', async () => {
|
|
295
|
+
mockSecretsManagerSend.mockRejectedValueOnce({
|
|
296
|
+
name: 'ResourceNotFoundException'
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await expect(
|
|
300
|
+
secretExists({ name: 'app/missing', region: 'us-east-1' })
|
|
301
|
+
).resolves.toBe(false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('gets current secret string value', async () => {
|
|
305
|
+
mockSecretsManagerSend.mockResolvedValueOnce({
|
|
306
|
+
SecretString: '{"API_KEY":"abc"}'
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await expect(
|
|
310
|
+
getSecretString({ name: 'app/existing', region: 'us-east-1' })
|
|
311
|
+
).resolves.toBe('{"API_KEY":"abc"}');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('rejects binary/non-string secrets for append/remove workflows', async () => {
|
|
315
|
+
mockSecretsManagerSend.mockResolvedValueOnce({
|
|
316
|
+
SecretBinary: new Uint8Array([1, 2, 3])
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await expect(
|
|
320
|
+
getSecretString({ name: 'app/existing', region: 'us-east-1' })
|
|
321
|
+
).rejects.toThrow('cannot be edited with append/remove');
|
|
322
|
+
});
|
|
323
|
+
|
|
281
324
|
it('deletes secret with recovery options', async () => {
|
|
282
325
|
const mockCredentials = jest.fn().mockResolvedValue({
|
|
283
326
|
accessKeyId: 'default',
|
package/dist/cli/helpers.js
CHANGED
|
@@ -9,7 +9,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.resolveAwsScope = exports.resolveSecretValue = exports.readStdin = exports.parseRecoveryDays = exports.printData = exports.renderTable = exports.asOutputFormat = void 0;
|
|
12
|
+
exports.resolveAwsScope = exports.parseEnvSecretsFile = exports.parseEnvSecrets = exports.resolveSecretValue = exports.readStdin = exports.parseRecoveryDays = exports.printData = exports.renderTable = exports.asOutputFormat = void 0;
|
|
13
|
+
const promises_1 = require("node:fs/promises");
|
|
13
14
|
const asOutputFormat = (value) => {
|
|
14
15
|
if (value !== 'json' && value !== 'table') {
|
|
15
16
|
throw new Error(`Invalid output format "${value}". Use "json" or "table".`);
|
|
@@ -81,9 +82,14 @@ const readStdin = (stdin = process.stdin) => __awaiter(void 0, void 0, void 0, f
|
|
|
81
82
|
});
|
|
82
83
|
});
|
|
83
84
|
exports.readStdin = readStdin;
|
|
84
|
-
const resolveSecretValue = (value, valueStdin) => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const resolveSecretValue = (value, valueStdin, valueFile) => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
+
const providedSources = [
|
|
87
|
+
value !== undefined,
|
|
88
|
+
valueStdin === true,
|
|
89
|
+
valueFile !== undefined
|
|
90
|
+
].filter(Boolean).length;
|
|
91
|
+
if (providedSources > 1) {
|
|
92
|
+
throw new Error('Use only one secret value source: --value, --value-stdin, or --file.');
|
|
87
93
|
}
|
|
88
94
|
if (valueStdin) {
|
|
89
95
|
if (process.stdin.isTTY) {
|
|
@@ -91,9 +97,65 @@ const resolveSecretValue = (value, valueStdin) => __awaiter(void 0, void 0, void
|
|
|
91
97
|
}
|
|
92
98
|
return yield (0, exports.readStdin)();
|
|
93
99
|
}
|
|
100
|
+
if (valueFile) {
|
|
101
|
+
const content = yield (0, promises_1.readFile)(valueFile, 'utf8');
|
|
102
|
+
return content.replace(/\r?\n$/, '');
|
|
103
|
+
}
|
|
94
104
|
return value;
|
|
95
105
|
});
|
|
96
106
|
exports.resolveSecretValue = resolveSecretValue;
|
|
107
|
+
const parseEnvLine = (line, lineNumber) => {
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const candidate = trimmed.startsWith('export ')
|
|
113
|
+
? trimmed.slice('export '.length).trimStart()
|
|
114
|
+
: trimmed;
|
|
115
|
+
const separatorIndex = candidate.indexOf('=');
|
|
116
|
+
if (separatorIndex <= 0) {
|
|
117
|
+
throw new Error(`Malformed env line ${lineNumber}. Expected KEY=value or export KEY=value.`);
|
|
118
|
+
}
|
|
119
|
+
const key = candidate.slice(0, separatorIndex).trim();
|
|
120
|
+
const value = candidate.slice(separatorIndex + 1).trim();
|
|
121
|
+
if (!key) {
|
|
122
|
+
throw new Error(`Malformed env line ${lineNumber}. Expected KEY=value or export KEY=value.`);
|
|
123
|
+
}
|
|
124
|
+
return { key, value };
|
|
125
|
+
};
|
|
126
|
+
const parseEnvSecrets = (content) => {
|
|
127
|
+
const seenKeys = new Set();
|
|
128
|
+
const entries = [];
|
|
129
|
+
const skipped = [];
|
|
130
|
+
const lines = content.split(/\r?\n/);
|
|
131
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
132
|
+
const parsed = parseEnvLine(lines[index], index + 1);
|
|
133
|
+
if (!parsed) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (seenKeys.has(parsed.key)) {
|
|
137
|
+
skipped.push({
|
|
138
|
+
key: parsed.key,
|
|
139
|
+
line: index + 1,
|
|
140
|
+
reason: 'duplicate key'
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
seenKeys.add(parsed.key);
|
|
145
|
+
entries.push({
|
|
146
|
+
key: parsed.key,
|
|
147
|
+
value: parsed.value,
|
|
148
|
+
line: index + 1
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return { entries, skipped };
|
|
152
|
+
};
|
|
153
|
+
exports.parseEnvSecrets = parseEnvSecrets;
|
|
154
|
+
const parseEnvSecretsFile = (path) => __awaiter(void 0, void 0, void 0, function* () {
|
|
155
|
+
const content = yield (0, promises_1.readFile)(path, 'utf8');
|
|
156
|
+
return (0, exports.parseEnvSecrets)(content);
|
|
157
|
+
});
|
|
158
|
+
exports.parseEnvSecretsFile = parseEnvSecretsFile;
|
|
97
159
|
const resolveAwsScope = (options, command) => {
|
|
98
160
|
var _a;
|
|
99
161
|
const globalOptions = ((_a = command === null || command === void 0 ? void 0 : command.optsWithGlobals) === null || _a === void 0 ? void 0 : _a.call(command)) || {};
|