@liflig/load-secrets 1.0.3

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 @@
1
+ * @capralifecycle/team-infra
@@ -0,0 +1,50 @@
1
+ name: ci
2
+ on:
3
+ push:
4
+ branches:
5
+ - "**"
6
+
7
+ defaults:
8
+ run:
9
+ # NOTE: A bit stricter than the default bash options used by GitHub Actions
10
+ # (bash --noprofile --norc -e -o pipefail {0})
11
+ shell: bash --noprofile --norc -euo pipefail {0}
12
+
13
+ # NOTE: Set concurrency for the current workflow to 1
14
+ concurrency: ci-${{ github.ref }}-${{ github.workflow }}
15
+
16
+ jobs:
17
+ build-and-release:
18
+ timeout-minutes: 60
19
+ runs-on: ubuntu-24.04
20
+ permissions:
21
+ contents: write
22
+ pull-requests: write
23
+ issues: write
24
+ steps:
25
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
26
+
27
+ - uses: capralifecycle/actions-lib/check-runtime-dependencies@7887a32ad872a79a8e00817659a30876f0a8f1be # v1.5.5
28
+
29
+ - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
30
+ with:
31
+ node-version: 22
32
+
33
+ - uses: capralifecycle/actions-lib/configure-npm@7887a32ad872a79a8e00817659a30876f0a8f1be # v1.5.5
34
+
35
+ - name: install dependencies
36
+ run: npm ci
37
+
38
+ - name: lint
39
+ run: npm run lint
40
+
41
+ - name: conditionally pack
42
+ if: ${{ github.ref != format('refs/heads/{0}', github.event.repository.default_branch) }}
43
+ run: npm pack
44
+
45
+ - name: conditionally semantic release
46
+ if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
47
+ env:
48
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
+ NPM_TOKEN: ${{ secrets.SHARED_NPMJS_TOKEN }}
50
+ run: npm run semantic-release
@@ -0,0 +1 @@
1
+ npx --no-install commitlint --edit "$1"
package/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ nodejs 22
package/Makefile ADDED
@@ -0,0 +1,9 @@
1
+ .PHONY: all
2
+ all: build
3
+
4
+ .PHONY: build
5
+ build:
6
+ @echo "Building..."
7
+ npm install
8
+ npm run build
9
+ npm run lint
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # load-secrets
2
+
3
+ Library for loading secrets into AWS Secrets Mananager from a configuration file.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ $ npm install @capraconsulting/load-secrets
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### 1. Create a script for loading secrets into AWS Secrets Manager
14
+
15
+ Create a script in your project for loading project secrets into AWS Secrets Manager.
16
+
17
+ Example: `load-secrets-demo-service.ts`
18
+
19
+ ```ts
20
+ // 1. Import the library
21
+ import { loadSecrets } from "@capraconsulting/load-secrets";
22
+
23
+ // 2. Define the secrets
24
+ const demoServiceApiKey: loadSecrets.Secret = {
25
+ name: "demo-service-api-key",
26
+ description: "API key for the demo service",
27
+ type: "string"
28
+ }
29
+
30
+ // 3. Assign the secrets to SecretGroups (collections of associations between secrets and accounts)
31
+ const secretGroups: loadSecrets.SecretGroup[] = [
32
+ {
33
+ accountId: "123412341234",
34
+ region: "eu-west-1",
35
+ description: "dev",
36
+ namePrefix: "/dev/demo-svc/",
37
+ secrets: [demoServiceApiKey]
38
+ },
39
+ {
40
+ accountId: "234523452345",
41
+ region: "eu-west-1",
42
+ description: "staging",
43
+ namePrefix: "/staging/demo-svc/",
44
+ secrets: [demoServiceApiKey]
45
+ }
46
+ ];
47
+
48
+ // 4. Load the secrets into AWS Secrets Manager
49
+ loadSecrets.loadSecretsCli({ secretGroups })
50
+ ```
51
+
52
+ ### 2. Assume an AWS role with the required permissions
53
+
54
+ Example: `aws-vault exec team-squirrel-demo-service`
55
+
56
+ Say, for example, that this command assumes AWS Account 123412341234.
57
+
58
+ ### 3. Run the script
59
+
60
+ ```bash
61
+ $ ./load-secrets-demo-service.ts
62
+ info Checking account for current credentials
63
+ info If any error is given, make sure you have valid credentials active
64
+ info Running for account 123412341234
65
+
66
+ Select secret to write:
67
+
68
+ dev (prefix: /dev/demo-svc/)
69
+ (0) demo-service-api-key (not yet created)
70
+
71
+ Enter index (or enter to quit): 0
72
+
73
+ Secret: /dev/demo-svc/demo-service-api-key
74
+ The secret does not already exist and will be created
75
+
76
+ Enter value (Ctrl+C to abort): test
77
+ Storing secret value:
78
+ test
79
+
80
+ Secret stored:
81
+ ARN: arn:aws:secretsmanager:eu-west-1:123412341234:secret:/dev/demo-svc/demo-service-api-key-UIqJ8N
82
+ Version: bdbf33c1-eda8-4aa1-b744-7c2006eae338
83
+
84
+ Select secret to write:
85
+
86
+ dev (prefix: /dev/demo-svc/)
87
+ 0) test-secret-delete-me (last changed 2024-12-03T13:50:44.237Z)
88
+
89
+ Enter index (or enter to quit):
90
+ ```
@@ -0,0 +1,3 @@
1
+ export default {
2
+ extends: ["@commitlint/config-conventional"],
3
+ };
@@ -0,0 +1,33 @@
1
+ import eslint from "@eslint/js";
2
+ import prettierPluginRecommended from "eslint-plugin-prettier/recommended";
3
+ import tseslint from "typescript-eslint";
4
+
5
+ export default tseslint.config(
6
+ {
7
+ ignores: ["lib/"],
8
+ },
9
+ {
10
+ extends: [
11
+ eslint.configs.recommended,
12
+ ...tseslint.configs.recommended,
13
+ // Must be last to override other rules
14
+ prettierPluginRecommended,
15
+ ],
16
+
17
+ rules: {
18
+ "@typescript-eslint/no-unused-vars": [
19
+ "error",
20
+ {
21
+ argsIgnorePattern: "^_",
22
+ varsIgnorePattern: "^_",
23
+ caughtErrorsIgnorePattern: "^_",
24
+ ignoreRestSiblings: true,
25
+ },
26
+ ],
27
+ "@typescript-eslint/no-explicit-any": "off",
28
+ "@typescript-eslint/consistent-type-imports": "error",
29
+ "@typescript-eslint/no-non-null-assertion": "off",
30
+ "@typescript-eslint/explicit-module-boundary-types": "off",
31
+ },
32
+ },
33
+ );
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@liflig/load-secrets",
3
+ "version": "1.0.3",
4
+ "description": "Library for loading project secrets into AWS Secrets Manager",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "lib/index.js",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/capralifecycle/load-secrets.git"
11
+ },
12
+ "scripts": {
13
+ "prepare": "husky",
14
+ "build": "tsc",
15
+ "lint": "eslint .",
16
+ "lint:fix": "eslint --fix .",
17
+ "semantic-release": "semantic-release"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@aws-sdk/client-secrets-manager": "3.699.0",
24
+ "@aws-sdk/client-sts": "3.699.0",
25
+ "chalk": "4.0.0",
26
+ "typescript": "5.7.2"
27
+ },
28
+ "devDependencies": {
29
+ "@commitlint/cli": "19.6.0",
30
+ "@commitlint/config-conventional": "19.6.0",
31
+ "@eslint/js": "9.16.0",
32
+ "@types/node": "22.10.1",
33
+ "@types/read": "0.0.32",
34
+ "eslint": "9.16.0",
35
+ "eslint-config-prettier": "9.1.0",
36
+ "eslint-plugin-prettier": "5.2.1",
37
+ "husky": "9.1.7",
38
+ "semantic-release": "24.2.0",
39
+ "typescript-eslint": "8.17.0"
40
+ }
41
+ }
@@ -0,0 +1,54 @@
1
+ import chalk from "chalk";
2
+ import readline from "readline";
3
+
4
+ const CLEAR_WHOLE_LINE = 0;
5
+
6
+ export function createReporter(argv: Record<string, unknown>): CLIReporter {
7
+ return new CLIReporter({
8
+ verbose: !!argv.verbose,
9
+ nonInteractive: !!argv.nonInteractive,
10
+ });
11
+ }
12
+
13
+ function clearLine(stdout: NodeJS.WriteStream) {
14
+ readline.clearLine(stdout, CLEAR_WHOLE_LINE);
15
+ readline.cursorTo(stdout, 0);
16
+ }
17
+
18
+ export class CLIReporter {
19
+ public constructor(
20
+ opts: {
21
+ nonInteractive?: boolean;
22
+ verbose?: boolean;
23
+ } = {},
24
+ ) {
25
+ this.nonInteractive = !!opts.nonInteractive;
26
+ this.isVerbose = !!opts.verbose;
27
+ }
28
+
29
+ public stdout = process.stdout;
30
+ public stderr = process.stderr;
31
+ public nonInteractive: boolean;
32
+ public isVerbose: boolean;
33
+ public format: typeof chalk = chalk;
34
+
35
+ public error(msg: string): void {
36
+ clearLine(this.stderr);
37
+ this.stderr.write(`${this.format.red("error")} ${msg}\n`);
38
+ }
39
+
40
+ public log(msg: string): void {
41
+ clearLine(this.stdout);
42
+ this.stdout.write(`${msg}\n`);
43
+ }
44
+
45
+ public warn(msg: string): void {
46
+ clearLine(this.stderr);
47
+ this.stderr.write(`${this.format.yellow("warning")} ${msg}\n`);
48
+ }
49
+
50
+ public info(msg: string): void {
51
+ clearLine(this.stdout);
52
+ this.stdout.write(`${this.format.blue("info")} ${msg}\n`);
53
+ }
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * as loadSecrets from "./load-secrets";
@@ -0,0 +1,2 @@
1
+ export { loadSecretsCli } from "./load-secrets";
2
+ export type { JsonSecret, Secret, SecretGroup } from "./types";
@@ -0,0 +1,463 @@
1
+ import type {
2
+ DescribeSecretResponse,
3
+ Tag,
4
+ } from "@aws-sdk/client-secrets-manager";
5
+ import {
6
+ CreateSecretCommand,
7
+ DescribeSecretCommand,
8
+ GetSecretValueCommand,
9
+ PutSecretValueCommand,
10
+ RestoreSecretCommand,
11
+ SecretsManagerClient,
12
+ TagResourceCommand,
13
+ UntagResourceCommand,
14
+ ReplicateSecretToRegionsCommand,
15
+ RemoveRegionsFromReplicationCommand,
16
+ } from "@aws-sdk/client-secrets-manager";
17
+ import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
18
+ import { ResourceNotFoundException } from "@aws-sdk/client-secrets-manager";
19
+ import read from "read";
20
+ import type { CLIReporter } from "../cli/reporter";
21
+ import { createReporter } from "../cli/reporter";
22
+
23
+ import type { JsonSecret, Secret, SecretGroup } from "./types";
24
+
25
+ class LoadSecrets {
26
+ private readonly smClientForRegions: Record<string, SecretsManagerClient> =
27
+ {};
28
+ private readonly stsClient: STSClient;
29
+
30
+ private readonly reporter: CLIReporter;
31
+ private readonly silent: boolean;
32
+
33
+ constructor(props: { reporter: CLIReporter; silent: boolean }) {
34
+ this.stsClient = new STSClient({
35
+ region: "eu-west-1",
36
+ });
37
+ this.reporter = props.reporter;
38
+ this.silent = props.silent;
39
+ }
40
+
41
+ private getSmClient(region: string): SecretsManagerClient {
42
+ if (!this.smClientForRegions[region]) {
43
+ this.smClientForRegions[region] = new SecretsManagerClient({
44
+ region,
45
+ });
46
+ }
47
+
48
+ return this.smClientForRegions[region];
49
+ }
50
+
51
+ async getInput(options: read.Options): Promise<string> {
52
+ return new Promise<string>((resolve, reject) => {
53
+ read(options, (err, answer) => {
54
+ if (err) {
55
+ reject(err);
56
+ }
57
+ resolve(answer);
58
+ });
59
+ });
60
+ }
61
+
62
+ async getSecretDetails(
63
+ client: SecretsManagerClient,
64
+ secretId: string,
65
+ ): Promise<DescribeSecretResponse | null> {
66
+ try {
67
+ return await client.send(
68
+ new DescribeSecretCommand({ SecretId: secretId }),
69
+ );
70
+ } catch (e) {
71
+ if (e instanceof ResourceNotFoundException) {
72
+ return null;
73
+ }
74
+
75
+ throw e;
76
+ }
77
+ }
78
+
79
+ async handleStringUpdate() {
80
+ return await this.getInput({
81
+ prompt: "Enter value (Ctrl+C to abort): ",
82
+ silent: this.silent,
83
+ });
84
+ }
85
+ async handleJsonUpdate(secret: JsonSecret) {
86
+ this.reporter.log("The secret is of type JSON with these expected fields:");
87
+ for (const field of secret.fields) {
88
+ const key = typeof field === "string" ? field : field.key;
89
+ const desc =
90
+ typeof field === "string"
91
+ ? ""
92
+ : field.description
93
+ ? ` (${field.description})`
94
+ : "";
95
+ this.reporter.log(` - ${key}${desc}`);
96
+ }
97
+
98
+ this.reporter.log("");
99
+
100
+ // TODO: Ability to specify full json value as one line.
101
+
102
+ const collectedValues: Record<string, string | null> = {};
103
+ for (const field of secret.fields) {
104
+ const key = typeof field === "string" ? field : field.key;
105
+
106
+ this.reporter.log(`Field: ${this.reporter.format.greenBright(key)}`);
107
+
108
+ if (typeof field !== "string" && field.example != null) {
109
+ this.reporter.log(
110
+ `Example: ${this.reporter.format.magentaBright(field.example)}`,
111
+ );
112
+ }
113
+
114
+ const value = await this.getInput({
115
+ prompt: "Enter value (Ctrl+C to abort): ",
116
+ silent: this.silent,
117
+ });
118
+ collectedValues[key] = value;
119
+
120
+ this.reporter.log("");
121
+ }
122
+
123
+ return JSON.stringify(collectedValues, undefined, " ");
124
+ }
125
+
126
+ getFullName(secretGroup: SecretGroup, secret: Secret) {
127
+ return `${secretGroup.namePrefix}${secret.name}`;
128
+ }
129
+
130
+ async syncTags(
131
+ client: SecretsManagerClient,
132
+ secret: DescribeSecretResponse,
133
+ tags: Tag[],
134
+ ) {
135
+ const keysToRemove = secret
136
+ .Tags!.filter(
137
+ (existingTag) => !tags.some((it) => it.Key! === existingTag.Key!),
138
+ )
139
+ .map((it) => it.Key!);
140
+
141
+ if (keysToRemove.length > 0) {
142
+ this.reporter.log(`Removing obsolete tags: ${keysToRemove.join(", ")}`);
143
+ await client.send(
144
+ new UntagResourceCommand({
145
+ SecretId: secret.ARN!,
146
+ TagKeys: keysToRemove,
147
+ }),
148
+ );
149
+ }
150
+
151
+ const tagsToUpdate = tags.filter((expectedTag) => {
152
+ const existing = secret.Tags!.find((it) => it.Key! === expectedTag.Key!);
153
+ return existing == null || existing.Value != expectedTag.Value;
154
+ });
155
+
156
+ if (tagsToUpdate.length > 0) {
157
+ this.reporter.log(
158
+ `Storing tags: ${tagsToUpdate.map((it) => it.Key!).join(", ")}`,
159
+ );
160
+ await client.send(
161
+ new TagResourceCommand({
162
+ SecretId: secret.ARN!,
163
+ Tags: tagsToUpdate,
164
+ }),
165
+ );
166
+ }
167
+ }
168
+
169
+ async getSecretValue(client: SecretsManagerClient, secretId: string) {
170
+ const result = await client.send(
171
+ new GetSecretValueCommand({
172
+ SecretId: secretId,
173
+ }),
174
+ );
175
+
176
+ if (result.SecretString == null) {
177
+ throw new Error("Missing SecretString (is it a binary?)");
178
+ }
179
+
180
+ return result.SecretString;
181
+ }
182
+
183
+ async handleUpdate(secretGroup: SecretGroup, secret: Secret) {
184
+ const client = this.getSmClient(secretGroup.region);
185
+
186
+ const fullName = this.getFullName(secretGroup, secret);
187
+ const describeSecret = await this.getSecretDetails(client, fullName);
188
+
189
+ this.reporter.log(`Secret: ${this.reporter.format.greenBright(fullName)}`);
190
+
191
+ if (describeSecret == null) {
192
+ this.reporter.log(
193
+ "The secret does not already exist and will be created",
194
+ );
195
+ } else {
196
+ this.reporter.log("Current value:");
197
+ this.reporter.log(
198
+ this.reporter.format.yellowBright(
199
+ (await this.getSecretValue(client, fullName)).replace(/^/gm, " "),
200
+ ),
201
+ );
202
+ }
203
+
204
+ this.reporter.log("");
205
+
206
+ let secretValue: string;
207
+
208
+ if (secret.type === "json") {
209
+ try {
210
+ secretValue = await this.handleJsonUpdate(secret);
211
+ } catch (e) {
212
+ if (e instanceof Error && e.message === "canceled") {
213
+ this.reporter.log("Aborted");
214
+ return;
215
+ }
216
+ throw e;
217
+ }
218
+ } else if (secret.type === "string") {
219
+ secretValue = await this.handleStringUpdate();
220
+ } else {
221
+ throw new Error(`Unsupported type`);
222
+ }
223
+
224
+ this.reporter.log("Storing secret value:");
225
+ this.reporter.log(
226
+ this.reporter.format.yellowBright(secretValue.replace(/^/gm, " ")),
227
+ );
228
+
229
+ const tags: Tag[] = [
230
+ {
231
+ Key: "Source",
232
+ Value: "load-secrets script",
233
+ },
234
+ ];
235
+
236
+ let arn: string;
237
+ let version: string;
238
+ let newReplicaRegions: string[] = [];
239
+ let removedReplicaRegions: string[] = [];
240
+
241
+ if (describeSecret == null) {
242
+ newReplicaRegions = secret.replicaRegions ?? [];
243
+ const createResult = await client.send(
244
+ new CreateSecretCommand({
245
+ Name: fullName,
246
+ AddReplicaRegions: secret.replicaRegions
247
+ ? secret.replicaRegions.map((replicaRegion) => ({
248
+ Region: replicaRegion,
249
+ }))
250
+ : undefined,
251
+ Description: "Created by load-secrets",
252
+ SecretString: secretValue,
253
+ Tags: tags,
254
+ }),
255
+ );
256
+
257
+ if (createResult.VersionId == null) {
258
+ throw new Error("Expected versionId");
259
+ }
260
+
261
+ arn = createResult.ARN!;
262
+ version = createResult.VersionId;
263
+ } else {
264
+ if (describeSecret.DeletedDate != null) {
265
+ await client.send(
266
+ new RestoreSecretCommand({
267
+ SecretId: fullName,
268
+ }),
269
+ );
270
+ }
271
+
272
+ const updateResult = await client.send(
273
+ new PutSecretValueCommand({
274
+ SecretId: fullName,
275
+ SecretString: secretValue,
276
+ }),
277
+ );
278
+ const currentReplicaRegions =
279
+ describeSecret.ReplicationStatus?.map(
280
+ (replicationStatus) => replicationStatus.Region,
281
+ ) ?? [];
282
+ newReplicaRegions =
283
+ secret.replicaRegions?.filter(
284
+ (region) => !currentReplicaRegions.includes(region),
285
+ ) ?? [];
286
+ removedReplicaRegions = currentReplicaRegions
287
+ .filter(
288
+ (region): region is string => !!region && typeof region === "string",
289
+ )
290
+ .filter((region) => !(secret.replicaRegions || []).includes(region));
291
+ if (newReplicaRegions.length > 0) {
292
+ await client.send(
293
+ new ReplicateSecretToRegionsCommand({
294
+ SecretId: fullName,
295
+ AddReplicaRegions: newReplicaRegions.map((region) => ({
296
+ Region: region,
297
+ })),
298
+ }),
299
+ );
300
+ }
301
+ if (removedReplicaRegions.length > 0) {
302
+ await client.send(
303
+ new RemoveRegionsFromReplicationCommand({
304
+ SecretId: fullName,
305
+ RemoveReplicaRegions: removedReplicaRegions,
306
+ }),
307
+ );
308
+ }
309
+
310
+ if (updateResult.VersionId == null) {
311
+ throw new Error("Expected versionId");
312
+ }
313
+
314
+ await this.syncTags(client, describeSecret, tags);
315
+
316
+ arn = updateResult.ARN!;
317
+ version = updateResult.VersionId;
318
+ }
319
+
320
+ this.reporter.log("");
321
+ this.reporter.log("Secret stored:");
322
+ this.reporter.log(`ARN: ${this.reporter.format.greenBright(arn)}`);
323
+ this.reporter.log(`Version: ${this.reporter.format.greenBright(version)}`);
324
+ if (newReplicaRegions.length > 0) {
325
+ this.reporter.log(
326
+ `Read replicas added to regions: ${newReplicaRegions
327
+ .map((r) => this.reporter.format.greenBright(r))
328
+ .join(", ")}`,
329
+ );
330
+ }
331
+ if (removedReplicaRegions.length > 0) {
332
+ this.reporter.log(
333
+ `Read replicas removed from regions: ${removedReplicaRegions
334
+ .map((r) => this.reporter.format.redBright(r))
335
+ .join(", ")}`,
336
+ );
337
+ }
338
+ }
339
+
340
+ checkSecretGroup(secretGroup: SecretGroup) {
341
+ if (
342
+ !secretGroup.namePrefix.startsWith("/") ||
343
+ !secretGroup.namePrefix.endsWith("/")
344
+ ) {
345
+ throw new Error(
346
+ `namePrefix should start and end with /. Current value: ${secretGroup.namePrefix}`,
347
+ );
348
+ }
349
+ }
350
+
351
+ getSecretDescription(details: DescribeSecretResponse | null) {
352
+ return details == null
353
+ ? "not yet created"
354
+ : details?.DeletedDate != null
355
+ ? `scheduled for deletion ${details.DeletedDate.toISOString()}`
356
+ : `last changed ${details.LastChangedDate?.toISOString() ?? "unknown"}`;
357
+ }
358
+
359
+ /**
360
+ * Returns false if aborted.
361
+ */
362
+ async selectAndUpdate(secretGroups: SecretGroup[]): Promise<boolean> {
363
+ const secrets: { secretGroup: SecretGroup; secret: Secret }[] = [];
364
+
365
+ this.reporter.log("Select secret to write:");
366
+ this.reporter.log("");
367
+
368
+ for (const secretGroup of secretGroups) {
369
+ this.reporter.log(
370
+ `${secretGroup.description} (prefix: ${secretGroup.namePrefix})`,
371
+ );
372
+
373
+ for (let i = 0; i < secretGroup.secrets.length; i++) {
374
+ const offset = secrets.length;
375
+ const secret = secretGroup.secrets[i];
376
+
377
+ secrets.push({
378
+ secret: secret,
379
+ secretGroup,
380
+ });
381
+
382
+ const client = this.getSmClient(secretGroup.region);
383
+ const details = await this.getSecretDetails(
384
+ client,
385
+ this.getFullName(secretGroup, secret),
386
+ );
387
+ const desc = this.getSecretDescription(details);
388
+
389
+ this.reporter.log(` (${offset}) ${secret.name} (${desc})`);
390
+ }
391
+ this.reporter.log("");
392
+ }
393
+
394
+ let index: number;
395
+ try {
396
+ const answer = await this.getInput({
397
+ prompt: "Enter index (or enter to quit): ",
398
+ });
399
+ if (answer.trim() === "") {
400
+ return false;
401
+ }
402
+
403
+ index = parseInt(answer);
404
+ if (!secrets[index]) {
405
+ throw new Error();
406
+ }
407
+ } catch (_) {
408
+ this.reporter.warn("Secret not found - aborting");
409
+ return false;
410
+ }
411
+
412
+ this.reporter.log("");
413
+ await this.handleUpdate(secrets[index].secretGroup, secrets[index].secret);
414
+ this.reporter.log("");
415
+
416
+ return true;
417
+ }
418
+
419
+ async process(secretGroups: SecretGroup[]) {
420
+ this.reporter.info("Checking account for current credentials");
421
+ this.reporter.info(
422
+ "If any error is given, make sure you have valid credentials active",
423
+ );
424
+ const currentAccount = await this.stsClient.send(
425
+ new GetCallerIdentityCommand({}),
426
+ );
427
+
428
+ this.reporter.info(`Running for account ${currentAccount.Account!}`);
429
+ this.reporter.log("");
430
+
431
+ const matchedSecretGroups = secretGroups.filter(
432
+ (it) => it.accountId === currentAccount.Account!,
433
+ );
434
+ if (matchedSecretGroups.length === 0) {
435
+ this.reporter.error(`No secrets specified for this account - aborting`);
436
+ return;
437
+ }
438
+
439
+ for (const secretGroup of matchedSecretGroups) {
440
+ this.checkSecretGroup(secretGroup);
441
+ }
442
+
443
+ // eslint-disable-next-line no-empty
444
+ while (await this.selectAndUpdate(matchedSecretGroups)) {}
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Load secrets interactively into Secrets Manager.
450
+ */
451
+ export function loadSecretsCli(props: { secretGroups: SecretGroup[] }): void {
452
+ const loadSecrets = new LoadSecrets({
453
+ reporter: createReporter({}),
454
+ // For now, we show the secrets, so that we get positive feedback that the value
455
+ // is correctly entered.
456
+ silent: false,
457
+ });
458
+
459
+ loadSecrets.process(props.secretGroups).catch((error) => {
460
+ console.error(error.stack || error.message || error);
461
+ process.exitCode = 1;
462
+ });
463
+ }
@@ -0,0 +1,40 @@
1
+ export interface BaseSecret {
2
+ name: string;
3
+ description?: string;
4
+ /**
5
+ * A list of regions to create read replicas
6
+ * of the secret in.
7
+ */
8
+ replicaRegions?: string[];
9
+ }
10
+
11
+ export type JsonSecretSimpleField = string;
12
+
13
+ export interface JsonSecretDescribedField {
14
+ key: string;
15
+ description?: string;
16
+ example?: string;
17
+ }
18
+
19
+ /**
20
+ * Used for secrets that are a single plaintext string,
21
+ * and do not require JSON formating.
22
+ */
23
+ export interface StringSecret extends BaseSecret {
24
+ type: "string";
25
+ }
26
+
27
+ export interface JsonSecret extends BaseSecret {
28
+ type: "json";
29
+ fields: (JsonSecretSimpleField | JsonSecretDescribedField)[];
30
+ }
31
+
32
+ export type Secret = JsonSecret | StringSecret;
33
+
34
+ export interface SecretGroup {
35
+ accountId: string;
36
+ region: string;
37
+ description: string;
38
+ namePrefix: string;
39
+ secrets: Secret[];
40
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "skipLibCheck": true,
5
+ "target": "ES2022",
6
+ "moduleDetection": "force",
7
+ "allowJs": true,
8
+ "resolveJsonModule": true,
9
+ "isolatedModules": true,
10
+ "strict": true,
11
+ "sourceMap": true,
12
+ "verbatimModuleSyntax": false,
13
+ "useUnknownInCatchVariables": false,
14
+
15
+ "noEmit": false,
16
+ "declaration": true,
17
+
18
+ "module": "preserve",
19
+ "outDir": "./lib",
20
+
21
+ "lib": ["es2022"]
22
+ },
23
+ "include": ["src"],
24
+ "exclude": ["cdk.out", "node_modules"]
25
+ }