env-secrets 0.5.0 → 0.5.2
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 +7 -1
- package/__e2e__/README.md +1 -1
- package/__e2e__/aws-cli-help.test.ts +23 -0
- package/__e2e__/aws-exec-args.test.ts +44 -0
- package/__e2e__/aws-get-secrets-args.test.ts +156 -0
- package/__e2e__/aws-output-file-args.test.ts +65 -0
- package/__e2e__/aws-secret-lifecycle.test.ts +316 -0
- package/__e2e__/aws-secret-mutation-args.test.ts +199 -0
- package/__e2e__/utils/aws-e2e-context.ts +50 -0
- package/__e2e__/utils/test-utils.ts +35 -39
- package/__tests__/index.test.ts +5 -3
- package/__tests__/vaults/secretsmanager.test.ts +47 -0
- package/dist/index.js +39 -3
- package/dist/vaults/secretsmanager.js +47 -8
- package/docs/AWS.md +7 -0
- package/package.json +5 -4
- package/src/index.ts +50 -3
- package/src/vaults/secretsmanager.ts +65 -7
- package/__e2e__/index.test.ts +0 -678
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
cliWithEnv,
|
|
7
|
+
cleanupTempFile,
|
|
8
|
+
execAwslocalCommand
|
|
9
|
+
} from './utils/test-utils';
|
|
10
|
+
import { registerAwsE2eContext } from './utils/aws-e2e-context';
|
|
11
|
+
|
|
12
|
+
describe('AWS Secret Mutation CLI Args', () => {
|
|
13
|
+
const { getLocalStackEnv } = registerAwsE2eContext();
|
|
14
|
+
|
|
15
|
+
test('should append and remove keys on a JSON secret', async () => {
|
|
16
|
+
const secretName = `managed-secret-append-remove-${Date.now()}`;
|
|
17
|
+
const tempFile = path.join(
|
|
18
|
+
os.tmpdir(),
|
|
19
|
+
`env-secrets-append-remove-${Date.now()}.env`
|
|
20
|
+
);
|
|
21
|
+
fs.writeFileSync(tempFile, 'API_KEY=first');
|
|
22
|
+
|
|
23
|
+
const createResult = await cliWithEnv(
|
|
24
|
+
[
|
|
25
|
+
'aws',
|
|
26
|
+
'secret',
|
|
27
|
+
'upsert',
|
|
28
|
+
'--file',
|
|
29
|
+
tempFile,
|
|
30
|
+
'--name',
|
|
31
|
+
secretName,
|
|
32
|
+
'--output',
|
|
33
|
+
'json'
|
|
34
|
+
],
|
|
35
|
+
getLocalStackEnv()
|
|
36
|
+
);
|
|
37
|
+
expect(createResult.code).toBe(0);
|
|
38
|
+
|
|
39
|
+
const appendResult = await cliWithEnv(
|
|
40
|
+
[
|
|
41
|
+
'aws',
|
|
42
|
+
'secret',
|
|
43
|
+
'append',
|
|
44
|
+
'-n',
|
|
45
|
+
secretName,
|
|
46
|
+
'--key',
|
|
47
|
+
'JIRA_EMAIL_TOKEN',
|
|
48
|
+
'-v',
|
|
49
|
+
'blah',
|
|
50
|
+
'--output',
|
|
51
|
+
'json'
|
|
52
|
+
],
|
|
53
|
+
getLocalStackEnv()
|
|
54
|
+
);
|
|
55
|
+
expect(appendResult.code).toBe(0);
|
|
56
|
+
|
|
57
|
+
const afterAppend = await execAwslocalCommand(
|
|
58
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
59
|
+
getLocalStackEnv()
|
|
60
|
+
);
|
|
61
|
+
expect(JSON.parse(afterAppend.stdout.trim())).toEqual({
|
|
62
|
+
API_KEY: 'first',
|
|
63
|
+
JIRA_EMAIL_TOKEN: 'blah'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const removeResult = await cliWithEnv(
|
|
67
|
+
[
|
|
68
|
+
'aws',
|
|
69
|
+
'secret',
|
|
70
|
+
'remove',
|
|
71
|
+
'-n',
|
|
72
|
+
secretName,
|
|
73
|
+
'--key',
|
|
74
|
+
'API_KEY',
|
|
75
|
+
'--output',
|
|
76
|
+
'json'
|
|
77
|
+
],
|
|
78
|
+
getLocalStackEnv()
|
|
79
|
+
);
|
|
80
|
+
expect(removeResult.code).toBe(0);
|
|
81
|
+
|
|
82
|
+
const afterRemove = await execAwslocalCommand(
|
|
83
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
84
|
+
getLocalStackEnv()
|
|
85
|
+
);
|
|
86
|
+
expect(JSON.parse(afterRemove.stdout.trim())).toEqual({
|
|
87
|
+
JIRA_EMAIL_TOKEN: 'blah'
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const deleteResult = await cliWithEnv(
|
|
91
|
+
[
|
|
92
|
+
'aws',
|
|
93
|
+
'secret',
|
|
94
|
+
'delete',
|
|
95
|
+
'-n',
|
|
96
|
+
secretName,
|
|
97
|
+
'--force-delete-without-recovery',
|
|
98
|
+
'--yes'
|
|
99
|
+
],
|
|
100
|
+
getLocalStackEnv()
|
|
101
|
+
);
|
|
102
|
+
expect(deleteResult.code).toBe(0);
|
|
103
|
+
cleanupTempFile(tempFile);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('should upsert secrets from env file', async () => {
|
|
107
|
+
const secretName = `e2e-upsert-${Date.now()}`;
|
|
108
|
+
const tempFile = path.join(
|
|
109
|
+
os.tmpdir(),
|
|
110
|
+
`env-secrets-upsert-${Date.now()}.env`
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
fs.writeFileSync(
|
|
114
|
+
tempFile,
|
|
115
|
+
['# sample', 'export API_KEY=first', 'DB_URL = postgres://one'].join('\n')
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const firstRun = await cliWithEnv(
|
|
119
|
+
[
|
|
120
|
+
'aws',
|
|
121
|
+
'secret',
|
|
122
|
+
'upsert',
|
|
123
|
+
'--file',
|
|
124
|
+
tempFile,
|
|
125
|
+
'--name',
|
|
126
|
+
secretName,
|
|
127
|
+
'--output',
|
|
128
|
+
'json'
|
|
129
|
+
],
|
|
130
|
+
getLocalStackEnv()
|
|
131
|
+
);
|
|
132
|
+
expect(firstRun.code).toBe(0);
|
|
133
|
+
const firstJson = JSON.parse(firstRun.stdout) as {
|
|
134
|
+
summary: { created: number; updated: number; skipped: number };
|
|
135
|
+
};
|
|
136
|
+
expect(firstJson.summary.created).toBe(1);
|
|
137
|
+
expect(firstJson.summary.updated).toBe(0);
|
|
138
|
+
|
|
139
|
+
const firstSecret = await execAwslocalCommand(
|
|
140
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
141
|
+
getLocalStackEnv()
|
|
142
|
+
);
|
|
143
|
+
expect(JSON.parse(firstSecret.stdout.trim())).toEqual({
|
|
144
|
+
API_KEY: 'first',
|
|
145
|
+
DB_URL: 'postgres://one'
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync(
|
|
149
|
+
tempFile,
|
|
150
|
+
['export API_KEY=second', 'DB_URL=postgres://two'].join('\n')
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const secondRun = await cliWithEnv(
|
|
154
|
+
[
|
|
155
|
+
'aws',
|
|
156
|
+
'secret',
|
|
157
|
+
'import',
|
|
158
|
+
'--file',
|
|
159
|
+
tempFile,
|
|
160
|
+
'--name',
|
|
161
|
+
secretName,
|
|
162
|
+
'--output',
|
|
163
|
+
'json'
|
|
164
|
+
],
|
|
165
|
+
getLocalStackEnv()
|
|
166
|
+
);
|
|
167
|
+
expect(secondRun.code).toBe(0);
|
|
168
|
+
const secondJson = JSON.parse(secondRun.stdout) as {
|
|
169
|
+
summary: { created: number; updated: number; skipped: number };
|
|
170
|
+
};
|
|
171
|
+
expect(secondJson.summary.created).toBe(0);
|
|
172
|
+
expect(secondJson.summary.updated).toBe(1);
|
|
173
|
+
expect(secondJson.summary.skipped).toBe(0);
|
|
174
|
+
|
|
175
|
+
const secondSecret = await execAwslocalCommand(
|
|
176
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
177
|
+
getLocalStackEnv()
|
|
178
|
+
);
|
|
179
|
+
expect(JSON.parse(secondSecret.stdout.trim())).toEqual({
|
|
180
|
+
API_KEY: 'second',
|
|
181
|
+
DB_URL: 'postgres://two'
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const deleteResult = await cliWithEnv(
|
|
185
|
+
[
|
|
186
|
+
'aws',
|
|
187
|
+
'secret',
|
|
188
|
+
'delete',
|
|
189
|
+
'-n',
|
|
190
|
+
secretName,
|
|
191
|
+
'--force-delete-without-recovery',
|
|
192
|
+
'--yes'
|
|
193
|
+
],
|
|
194
|
+
getLocalStackEnv()
|
|
195
|
+
);
|
|
196
|
+
expect(deleteResult.code).toBe(0);
|
|
197
|
+
cleanupTempFile(tempFile);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LocalStackHelper,
|
|
3
|
+
createTestProfile,
|
|
4
|
+
restoreTestProfile,
|
|
5
|
+
TestProfileContext,
|
|
6
|
+
TestSecret,
|
|
7
|
+
CreatedSecret
|
|
8
|
+
} from './test-utils';
|
|
9
|
+
|
|
10
|
+
export interface AwsE2eContext {
|
|
11
|
+
createTestSecret: (
|
|
12
|
+
secret: TestSecret,
|
|
13
|
+
region?: string
|
|
14
|
+
) => Promise<CreatedSecret>;
|
|
15
|
+
getLocalStackEnv: (
|
|
16
|
+
overrides?: Record<string, string>
|
|
17
|
+
) => Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function registerAwsE2eContext(): AwsE2eContext {
|
|
21
|
+
let localStack: LocalStackHelper;
|
|
22
|
+
let profileContext: TestProfileContext | undefined;
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
localStack = new LocalStackHelper();
|
|
26
|
+
await localStack.waitForLocalStack();
|
|
27
|
+
profileContext = createTestProfile();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
await localStack.cleanupRunSecrets();
|
|
32
|
+
restoreTestProfile(profileContext);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
createTestSecret: async (
|
|
37
|
+
secret: TestSecret,
|
|
38
|
+
region?: string
|
|
39
|
+
): Promise<CreatedSecret> => {
|
|
40
|
+
return await localStack.createSecret(secret, region);
|
|
41
|
+
},
|
|
42
|
+
getLocalStackEnv: (overrides: Record<string, string> = {}) => ({
|
|
43
|
+
AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
|
|
44
|
+
AWS_ACCESS_KEY_ID: 'test',
|
|
45
|
+
AWS_SECRET_ACCESS_KEY: 'test',
|
|
46
|
+
AWS_DEFAULT_REGION: 'us-east-1',
|
|
47
|
+
...overrides
|
|
48
|
+
})
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -584,26 +584,11 @@ export function cleanupTempFile(filePath: string): void {
|
|
|
584
584
|
}
|
|
585
585
|
|
|
586
586
|
export function createTestProfile() {
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
const
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
// Create .aws directory if it doesn't exist
|
|
593
|
-
if (!fs.existsSync(awsDir)) {
|
|
594
|
-
fs.mkdirSync(awsDir, { mode: 0o700 });
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Backup existing files if they exist
|
|
598
|
-
const backupCredentials = credentialsFile + '.backup';
|
|
599
|
-
const backupConfig = configFile + '.backup';
|
|
600
|
-
|
|
601
|
-
if (fs.existsSync(credentialsFile)) {
|
|
602
|
-
fs.copyFileSync(credentialsFile, backupCredentials);
|
|
603
|
-
}
|
|
604
|
-
if (fs.existsSync(configFile)) {
|
|
605
|
-
fs.copyFileSync(configFile, backupConfig);
|
|
606
|
-
}
|
|
587
|
+
const tempAwsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'env-secrets-aws-'));
|
|
588
|
+
const credentialsFile = path.join(tempAwsDir, 'credentials');
|
|
589
|
+
const configFile = path.join(tempAwsDir, 'config');
|
|
590
|
+
const previousCredentialsFile = process.env.AWS_SHARED_CREDENTIALS_FILE;
|
|
591
|
+
const previousConfigFile = process.env.AWS_CONFIG_FILE;
|
|
607
592
|
|
|
608
593
|
// Create test profile
|
|
609
594
|
const credentialsContent = `[default]
|
|
@@ -625,34 +610,45 @@ region = us-east-1
|
|
|
625
610
|
fs.writeFileSync(credentialsFile, credentialsContent, { mode: 0o600 });
|
|
626
611
|
fs.writeFileSync(configFile, configContent, { mode: 0o600 });
|
|
627
612
|
|
|
628
|
-
|
|
613
|
+
process.env.AWS_SHARED_CREDENTIALS_FILE = credentialsFile;
|
|
614
|
+
process.env.AWS_CONFIG_FILE = configFile;
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
tempAwsDir,
|
|
618
|
+
previousCredentialsFile,
|
|
619
|
+
previousConfigFile
|
|
620
|
+
};
|
|
629
621
|
}
|
|
630
622
|
|
|
631
|
-
export
|
|
632
|
-
|
|
633
|
-
|
|
623
|
+
export interface TestProfileContext {
|
|
624
|
+
tempAwsDir: string;
|
|
625
|
+
previousCredentialsFile?: string;
|
|
626
|
+
previousConfigFile?: string;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function restoreTestProfile(
|
|
630
|
+
profileContext: TestProfileContext | undefined
|
|
631
|
+
): void {
|
|
632
|
+
if (!profileContext) {
|
|
633
|
+
debugWarn('No test profile context provided for profile restoration');
|
|
634
634
|
return;
|
|
635
635
|
}
|
|
636
636
|
|
|
637
|
-
const credentialsFile = path.join(awsDir, 'credentials');
|
|
638
|
-
const configFile = path.join(awsDir, 'config');
|
|
639
|
-
const backupCredentials = credentialsFile + '.backup';
|
|
640
|
-
const backupConfig = configFile + '.backup';
|
|
641
|
-
|
|
642
637
|
try {
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
} else
|
|
647
|
-
|
|
638
|
+
if (profileContext.previousCredentialsFile) {
|
|
639
|
+
process.env.AWS_SHARED_CREDENTIALS_FILE =
|
|
640
|
+
profileContext.previousCredentialsFile;
|
|
641
|
+
} else {
|
|
642
|
+
delete process.env.AWS_SHARED_CREDENTIALS_FILE;
|
|
648
643
|
}
|
|
649
644
|
|
|
650
|
-
if (
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
fs.unlinkSync(configFile);
|
|
645
|
+
if (profileContext.previousConfigFile) {
|
|
646
|
+
process.env.AWS_CONFIG_FILE = profileContext.previousConfigFile;
|
|
647
|
+
} else {
|
|
648
|
+
delete process.env.AWS_CONFIG_FILE;
|
|
655
649
|
}
|
|
650
|
+
|
|
651
|
+
fs.rmSync(profileContext.tempAwsDir, { recursive: true, force: true });
|
|
656
652
|
} catch (error) {
|
|
657
653
|
debugWarn('Failed to restore AWS profile:', error);
|
|
658
654
|
}
|
package/__tests__/index.test.ts
CHANGED
|
@@ -30,9 +30,11 @@ const mockWriteFileSync = writeFileSync as jest.MockedFunction<
|
|
|
30
30
|
>;
|
|
31
31
|
const mockExistsSync = existsSync as jest.MockedFunction<typeof existsSync>;
|
|
32
32
|
const mockDebug = Debug as jest.MockedFunction<typeof Debug>;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
interface SecretsmanagerMockFn {
|
|
34
|
+
(options: Record<string, unknown>): Promise<Record<string, string>>;
|
|
35
|
+
}
|
|
36
|
+
const mockSecretsmanager =
|
|
37
|
+
secretsmanager as unknown as jest.MockedFunction<SecretsmanagerMockFn>;
|
|
36
38
|
const mockObjectToExport = objectToExport as jest.MockedFunction<
|
|
37
39
|
typeof objectToExport
|
|
38
40
|
>;
|
|
@@ -247,6 +247,40 @@ describe('secretsmanager', () => {
|
|
|
247
247
|
expect(result).toEqual({});
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
it('should parse dotenv style secret values', async () => {
|
|
251
|
+
mockSecretsManagerSend.mockResolvedValueOnce({
|
|
252
|
+
SecretString:
|
|
253
|
+
'GITHUB_PAT=github_pat_123\nexport API_URL=https://example.com'
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const result = await secretsmanager({
|
|
257
|
+
secret: 'my-secret',
|
|
258
|
+
region: 'us-east-1'
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(result).toEqual({
|
|
262
|
+
GITHUB_PAT: 'github_pat_123',
|
|
263
|
+
API_URL: 'https://example.com'
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should parse JSON-encoded dotenv style secret values', async () => {
|
|
268
|
+
mockSecretsManagerSend.mockResolvedValueOnce({
|
|
269
|
+
SecretString:
|
|
270
|
+
'"GITHUB_PAT=github_pat_123\\nexport API_URL=https://example.com"'
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const result = await secretsmanager({
|
|
274
|
+
secret: 'my-secret',
|
|
275
|
+
region: 'us-east-1'
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(result).toEqual({
|
|
279
|
+
GITHUB_PAT: 'github_pat_123',
|
|
280
|
+
API_URL: 'https://example.com'
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
250
284
|
it('should handle empty secret values', async () => {
|
|
251
285
|
mockSecretsManagerSend.mockResolvedValueOnce({
|
|
252
286
|
SecretString: ''
|
|
@@ -286,6 +320,19 @@ describe('secretsmanager', () => {
|
|
|
286
320
|
expect(result).toEqual({});
|
|
287
321
|
});
|
|
288
322
|
|
|
323
|
+
it('should return empty object for malformed dotenv lines', async () => {
|
|
324
|
+
mockSecretsManagerSend.mockResolvedValueOnce({
|
|
325
|
+
SecretString: 'NOT_VALID_LINE'
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const result = await secretsmanager({
|
|
329
|
+
secret: 'my-secret',
|
|
330
|
+
region: 'us-east-1'
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
expect(result).toEqual({});
|
|
334
|
+
});
|
|
335
|
+
|
|
289
336
|
it('should handle secrets with special characters', async () => {
|
|
290
337
|
mockSecretsManagerSend.mockResolvedValueOnce({
|
|
291
338
|
SecretString: '{"SPECIAL_KEY": "value with spaces & symbols!@#$%"}'
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,40 @@ const parseSecretJsonObject = (secretName, value) => {
|
|
|
43
43
|
}
|
|
44
44
|
return parsed;
|
|
45
45
|
};
|
|
46
|
+
const parseEnvToObject = (value) => {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = (0, helpers_1.parseEnvSecrets)(value);
|
|
49
|
+
if (parsed.entries.length === 0) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return Object.fromEntries(parsed.entries.map((entry) => [entry.key, entry.value]));
|
|
53
|
+
}
|
|
54
|
+
catch (_a) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const toSecretJsonObject = (value) => {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(value);
|
|
61
|
+
if (parsed && !Array.isArray(parsed) && typeof parsed === 'object') {
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
if (typeof parsed === 'string') {
|
|
65
|
+
const envPayload = parseEnvToObject(parsed);
|
|
66
|
+
if (envPayload) {
|
|
67
|
+
return envPayload;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { value: parsed };
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
const envPayload = parseEnvToObject(value);
|
|
74
|
+
if (envPayload) {
|
|
75
|
+
return envPayload;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { value };
|
|
79
|
+
};
|
|
46
80
|
// main program
|
|
47
81
|
program
|
|
48
82
|
.name('env-secrets')
|
|
@@ -63,6 +97,7 @@ const awsCommand = program
|
|
|
63
97
|
}
|
|
64
98
|
const secrets = yield (0, secretsmanager_1.secretsmanager)(options);
|
|
65
99
|
debug(secrets);
|
|
100
|
+
const envSecrets = Object.fromEntries(Object.entries(secrets).map(([key, value]) => [key, String(value)]));
|
|
66
101
|
if (options.output) {
|
|
67
102
|
// Check if file already exists
|
|
68
103
|
if ((0, node_fs_1.existsSync)(options.output)) {
|
|
@@ -71,14 +106,14 @@ const awsCommand = program
|
|
|
71
106
|
process.exit(1);
|
|
72
107
|
}
|
|
73
108
|
// Write secrets to file with 0400 permissions
|
|
74
|
-
const envContent = (0, utils_1.objectToExport)(
|
|
109
|
+
const envContent = (0, utils_1.objectToExport)(envSecrets);
|
|
75
110
|
(0, node_fs_1.writeFileSync)(options.output, envContent, { mode: 0o400 });
|
|
76
111
|
// eslint-disable-next-line no-console
|
|
77
112
|
console.log(`Secrets written to ${options.output}`);
|
|
78
113
|
}
|
|
79
114
|
else {
|
|
80
115
|
// Original behavior: merge secrets into environment and run program
|
|
81
|
-
const env = Object.assign({}, process.env,
|
|
116
|
+
const env = Object.assign({}, process.env, envSecrets);
|
|
82
117
|
debug(env);
|
|
83
118
|
if (program && program.length > 0) {
|
|
84
119
|
debug(`${program[0]} ${program.slice(1)}`);
|
|
@@ -124,9 +159,10 @@ secretCommand
|
|
|
124
159
|
if (!value) {
|
|
125
160
|
throw new Error('Secret value is required. Provide --value, --value-stdin, or --file.');
|
|
126
161
|
}
|
|
162
|
+
const payload = toSecretJsonObject(value);
|
|
127
163
|
const result = yield (0, secretsmanager_admin_1.createSecret)({
|
|
128
164
|
name: options.name,
|
|
129
|
-
value,
|
|
165
|
+
value: JSON.stringify(payload),
|
|
130
166
|
description: options.description,
|
|
131
167
|
kmsKeyId: options.kmsKeyId,
|
|
132
168
|
tags: options.tag,
|
|
@@ -17,7 +17,52 @@ const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
|
|
|
17
17
|
const client_sts_1 = require("@aws-sdk/client-sts");
|
|
18
18
|
const debug_1 = __importDefault(require("debug"));
|
|
19
19
|
const aws_config_1 = require("./aws-config");
|
|
20
|
+
const helpers_1 = require("../cli/helpers");
|
|
20
21
|
const debug = (0, debug_1.default)('env-secrets:secretsmanager');
|
|
22
|
+
const isSecretValue = (value) => {
|
|
23
|
+
return (typeof value === 'string' ||
|
|
24
|
+
typeof value === 'number' ||
|
|
25
|
+
typeof value === 'boolean');
|
|
26
|
+
};
|
|
27
|
+
const asSecretRecord = (value) => {
|
|
28
|
+
if (!value || Array.isArray(value) || typeof value !== 'object') {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
return Object.entries(value).reduce((result, [key, entryValue]) => {
|
|
32
|
+
if (isSecretValue(entryValue)) {
|
|
33
|
+
result[key] = entryValue;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}, {});
|
|
37
|
+
};
|
|
38
|
+
const parseSecretString = (secretvalue) => {
|
|
39
|
+
const parseAsEnvRecord = (envSource) => {
|
|
40
|
+
try {
|
|
41
|
+
const parsedEnv = (0, helpers_1.parseEnvSecrets)(envSource);
|
|
42
|
+
if (parsedEnv.entries.length === 0) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
return Object.fromEntries(parsedEnv.entries.map((entry) => [entry.key, entry.value]));
|
|
46
|
+
}
|
|
47
|
+
catch (_a) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const parsedJson = JSON.parse(secretvalue);
|
|
53
|
+
const parsedRecord = asSecretRecord(parsedJson);
|
|
54
|
+
if (Object.keys(parsedRecord).length > 0) {
|
|
55
|
+
return parsedRecord;
|
|
56
|
+
}
|
|
57
|
+
if (typeof parsedJson === 'string') {
|
|
58
|
+
return parseAsEnvRecord(parsedJson);
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
catch (_a) {
|
|
63
|
+
return parseAsEnvRecord(secretvalue);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
21
66
|
const isCredentialsError = (error) => {
|
|
22
67
|
if (!error || typeof error !== 'object') {
|
|
23
68
|
return false;
|
|
@@ -68,14 +113,8 @@ const secretsmanager = (options) => __awaiter(void 0, void 0, void 0, function*
|
|
|
68
113
|
});
|
|
69
114
|
const response = yield client.send(command);
|
|
70
115
|
const secretvalue = response.SecretString;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return JSON.parse(secretvalue);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch (err) {
|
|
77
|
-
// eslint-disable-next-line no-console
|
|
78
|
-
console.error(err);
|
|
116
|
+
if (secretvalue) {
|
|
117
|
+
return parseSecretString(secretvalue);
|
|
79
118
|
}
|
|
80
119
|
}
|
|
81
120
|
catch (err) {
|
package/docs/AWS.md
CHANGED
|
@@ -200,6 +200,12 @@ source secrets.env
|
|
|
200
200
|
--output json
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
+
`create` always writes a JSON object:
|
|
204
|
+
|
|
205
|
+
- object JSON input is preserved semantically as an object (formatting and key order may change) (`{"API_KEY":"abc123"}`)
|
|
206
|
+
- dotenv-style input is converted (`KEY=value` -> `{"KEY":"value"}`)
|
|
207
|
+
- non-object/scalar input is wrapped (`super-secret-value` -> `{"value":"super-secret-value"}`)
|
|
208
|
+
|
|
203
209
|
2. **Create from stdin (recommended for sensitive values):**
|
|
204
210
|
|
|
205
211
|
```bash
|
|
@@ -262,6 +268,7 @@ source secrets.env
|
|
|
262
268
|
|
|
263
269
|
- `delete` requires `--yes`.
|
|
264
270
|
- `create`/`update` accept `--value`, `--value-stdin`, or `--file` (use only one).
|
|
271
|
+
- `create` always stores `SecretString` as a JSON object.
|
|
265
272
|
- `append` and `remove` require the secret value to be a JSON object.
|
|
266
273
|
- `upsert/import --file --name` parses `export KEY=value` and `KEY=value`, stores them as one JSON secret object, ignores blank lines/comments, and reports `created`, `updated`, `skipped`, and `failed`.
|
|
267
274
|
- Use `--value-stdin` to avoid shell history leakage for sensitive values.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "env-secrets",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "get secrets from a secrets vault and inject them into the running environment",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Mark C Allen (@markcallen)",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"test:unit": "jest __tests__",
|
|
27
27
|
"test:unit:coverage": "jest __tests__ --coverage",
|
|
28
28
|
"test:e2e": "npm run build && jest --config jest.e2e.config.js",
|
|
29
|
+
"test:e2e:coverage": "npm run build && jest --config jest.e2e.config.js --coverage --coverageDirectory=coverage-e2e",
|
|
29
30
|
"test:e2e:debug": "npm run build && DEBUG=true jest --config jest.e2e.config.js"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
@@ -52,9 +53,9 @@
|
|
|
52
53
|
"typescript": "^4.9.5"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@aws-sdk/client-secrets-manager": "^3.
|
|
56
|
-
"@aws-sdk/client-sts": "^3.
|
|
57
|
-
"@aws-sdk/credential-providers": "^3.
|
|
56
|
+
"@aws-sdk/client-secrets-manager": "^3.1004.0",
|
|
57
|
+
"@aws-sdk/client-sts": "^3.1004.0",
|
|
58
|
+
"@aws-sdk/credential-providers": "^3.1004.0",
|
|
58
59
|
"commander": "^9.5.0",
|
|
59
60
|
"debug": "^4.4.3"
|
|
60
61
|
},
|