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.
@@ -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 homeDir = os.homedir();
588
- const awsDir = path.join(homeDir, '.aws');
589
- const credentialsFile = path.join(awsDir, 'credentials');
590
- const configFile = path.join(awsDir, 'config');
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
- return awsDir;
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 function restoreTestProfile(awsDir: string | undefined): void {
632
- if (!awsDir) {
633
- debugWarn('No AWS directory provided for profile restoration');
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 (fs.existsSync(backupCredentials)) {
644
- fs.copyFileSync(backupCredentials, credentialsFile);
645
- fs.unlinkSync(backupCredentials);
646
- } else if (fs.existsSync(credentialsFile)) {
647
- fs.unlinkSync(credentialsFile);
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 (fs.existsSync(backupConfig)) {
651
- fs.copyFileSync(backupConfig, configFile);
652
- fs.unlinkSync(backupConfig);
653
- } else if (fs.existsSync(configFile)) {
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
  }
@@ -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
- const mockSecretsmanager = secretsmanager as jest.MockedFunction<
34
- typeof secretsmanager
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)(secrets);
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, secrets);
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
- try {
72
- if (secretvalue) {
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.0",
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.996.0",
56
- "@aws-sdk/client-sts": "^3.996.0",
57
- "@aws-sdk/credential-providers": "^3.996.0",
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
  },