env-secrets 0.3.3 → 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 +12 -3
- package/README.md +52 -8
- package/__e2e__/README.md +2 -5
- package/__e2e__/index.test.ts +341 -2
- package/__e2e__/utils/test-utils.ts +61 -1
- package/__tests__/cli/helpers.test.ts +217 -0
- package/__tests__/vaults/aws-config.test.ts +85 -0
- package/__tests__/vaults/secretsmanager-admin.test.ts +355 -0
- package/dist/cli/helpers.js +172 -0
- package/dist/index.js +456 -2
- package/dist/vaults/aws-config.js +29 -0
- package/dist/vaults/secretsmanager-admin.js +273 -0
- package/dist/vaults/secretsmanager.js +8 -16
- package/docs/AWS.md +129 -2
- package/jest.e2e.config.js +1 -0
- package/package.json +5 -5
- package/src/cli/helpers.ts +239 -0
- package/src/index.ts +595 -2
- package/src/vaults/aws-config.ts +51 -0
- package/src/vaults/secretsmanager-admin.ts +397 -0
- package/src/vaults/secretsmanager.ts +8 -21
- package/website/docs/cli-reference.mdx +101 -0
- package/website/docs/providers/aws-secrets-manager.mdx +59 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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");
|
|
14
|
+
const asOutputFormat = (value) => {
|
|
15
|
+
if (value !== 'json' && value !== 'table') {
|
|
16
|
+
throw new Error(`Invalid output format "${value}". Use "json" or "table".`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
20
|
+
exports.asOutputFormat = asOutputFormat;
|
|
21
|
+
const renderTable = (headers, rows) => {
|
|
22
|
+
if (rows.length === 0) {
|
|
23
|
+
return 'No results.';
|
|
24
|
+
}
|
|
25
|
+
const widths = headers.map((header) => {
|
|
26
|
+
return Math.max(header.label.length, ...rows.map((row) => String(row[header.key] || '').length));
|
|
27
|
+
});
|
|
28
|
+
const headerLine = headers
|
|
29
|
+
.map((header, index) => header.label.padEnd(widths[index]))
|
|
30
|
+
.join(' ');
|
|
31
|
+
const divider = headers
|
|
32
|
+
.map((_, index) => '-'.repeat(widths[index]))
|
|
33
|
+
.join(' ');
|
|
34
|
+
const lines = rows.map((row) => headers
|
|
35
|
+
.map((header, index) => String(row[header.key] || '').padEnd(widths[index]))
|
|
36
|
+
.join(' '));
|
|
37
|
+
return [headerLine, divider, ...lines].join('\n');
|
|
38
|
+
};
|
|
39
|
+
exports.renderTable = renderTable;
|
|
40
|
+
const printData = (format, headers, rows) => {
|
|
41
|
+
if (format === 'json') {
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
console.log((0, exports.renderTable)(headers, rows));
|
|
48
|
+
};
|
|
49
|
+
exports.printData = printData;
|
|
50
|
+
const parseRecoveryDays = (value) => {
|
|
51
|
+
const parsed = Number(value);
|
|
52
|
+
if (!Number.isInteger(parsed) || parsed < 7 || parsed > 30) {
|
|
53
|
+
throw new Error('Recovery days must be an integer between 7 and 30.');
|
|
54
|
+
}
|
|
55
|
+
return parsed;
|
|
56
|
+
};
|
|
57
|
+
exports.parseRecoveryDays = parseRecoveryDays;
|
|
58
|
+
const readStdin = (stdin = process.stdin) => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
|
+
const chunks = [];
|
|
60
|
+
return yield new Promise((resolve, reject) => {
|
|
61
|
+
const onData = (chunk) => {
|
|
62
|
+
chunks.push(chunk);
|
|
63
|
+
};
|
|
64
|
+
const onEnd = () => {
|
|
65
|
+
cleanup();
|
|
66
|
+
resolve(Buffer.concat(chunks)
|
|
67
|
+
.toString('utf8')
|
|
68
|
+
.replace(/\r?\n$/, ''));
|
|
69
|
+
};
|
|
70
|
+
const onError = (error) => {
|
|
71
|
+
cleanup();
|
|
72
|
+
reject(error);
|
|
73
|
+
};
|
|
74
|
+
const cleanup = () => {
|
|
75
|
+
stdin.off('data', onData);
|
|
76
|
+
stdin.off('end', onEnd);
|
|
77
|
+
stdin.off('error', onError);
|
|
78
|
+
};
|
|
79
|
+
stdin.on('data', onData);
|
|
80
|
+
stdin.once('end', onEnd);
|
|
81
|
+
stdin.once('error', onError);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
exports.readStdin = readStdin;
|
|
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.');
|
|
93
|
+
}
|
|
94
|
+
if (valueStdin) {
|
|
95
|
+
if (process.stdin.isTTY) {
|
|
96
|
+
throw new Error('No stdin detected. Pipe a value when using --value-stdin.');
|
|
97
|
+
}
|
|
98
|
+
return yield (0, exports.readStdin)();
|
|
99
|
+
}
|
|
100
|
+
if (valueFile) {
|
|
101
|
+
const content = yield (0, promises_1.readFile)(valueFile, 'utf8');
|
|
102
|
+
return content.replace(/\r?\n$/, '');
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
});
|
|
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;
|
|
159
|
+
const resolveAwsScope = (options, command) => {
|
|
160
|
+
var _a;
|
|
161
|
+
const globalOptions = ((_a = command === null || command === void 0 ? void 0 : command.optsWithGlobals) === null || _a === void 0 ? void 0 : _a.call(command)) || {};
|
|
162
|
+
const profile = options.profile ||
|
|
163
|
+
(typeof globalOptions.profile === 'string'
|
|
164
|
+
? globalOptions.profile
|
|
165
|
+
: undefined);
|
|
166
|
+
const region = options.region ||
|
|
167
|
+
(typeof globalOptions.region === 'string'
|
|
168
|
+
? globalOptions.region
|
|
169
|
+
: undefined);
|
|
170
|
+
return { profile, region };
|
|
171
|
+
};
|
|
172
|
+
exports.resolveAwsScope = resolveAwsScope;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
/* istanbul ignore file */
|
|
3
4
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
4
5
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
5
6
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -19,24 +20,47 @@ const node_fs_1 = require("node:fs");
|
|
|
19
20
|
const debug_1 = __importDefault(require("debug"));
|
|
20
21
|
const version_1 = require("./version");
|
|
21
22
|
const secretsmanager_1 = require("./vaults/secretsmanager");
|
|
23
|
+
const secretsmanager_admin_1 = require("./vaults/secretsmanager-admin");
|
|
24
|
+
const helpers_1 = require("./cli/helpers");
|
|
22
25
|
const utils_1 = require("./vaults/utils");
|
|
23
26
|
const debug = (0, debug_1.default)('env-secrets');
|
|
24
27
|
const program = new commander_1.Command();
|
|
28
|
+
const exitWithError = (error) => {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
};
|
|
33
|
+
const parseSecretJsonObject = (secretName, value) => {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = JSON.parse(value);
|
|
37
|
+
}
|
|
38
|
+
catch (_a) {
|
|
39
|
+
throw new Error(`Secret "${secretName}" is not valid JSON. append/remove requires a JSON object secret.`);
|
|
40
|
+
}
|
|
41
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
|
42
|
+
throw new Error(`Secret "${secretName}" must be a JSON object. append/remove does not support arrays or scalar values.`);
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
};
|
|
25
46
|
// main program
|
|
26
47
|
program
|
|
27
48
|
.name('env-secrets')
|
|
28
49
|
.description('pull secrets from vaults and inject them into the running environment')
|
|
29
50
|
.version(version_1.LIB_VERSION);
|
|
30
51
|
// aws secretsmanager
|
|
31
|
-
program
|
|
52
|
+
const awsCommand = program
|
|
32
53
|
.command('aws')
|
|
33
54
|
.description('get secrets from AWS secrets manager')
|
|
34
55
|
.addArgument(new commander_1.Argument('[program...]', 'program to run'))
|
|
35
|
-
.
|
|
56
|
+
.option('-s, --secret <secret>', 'secret to get')
|
|
36
57
|
.option('-p, --profile <profile>', 'profile to use')
|
|
37
58
|
.option('-r, --region <region>', 'region to use')
|
|
38
59
|
.option('-o, --output <file>', 'output secrets to file instead of environment variables')
|
|
39
60
|
.action((program, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
if (!options.secret) {
|
|
62
|
+
exitWithError(new Error('Missing required option --secret for this command.'));
|
|
63
|
+
}
|
|
40
64
|
const secrets = yield (0, secretsmanager_1.secretsmanager)(options);
|
|
41
65
|
debug(secrets);
|
|
42
66
|
if (options.output) {
|
|
@@ -72,4 +96,434 @@ program
|
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
}));
|
|
99
|
+
const secretCommand = awsCommand
|
|
100
|
+
.command('secret')
|
|
101
|
+
.description('manage AWS secrets');
|
|
102
|
+
secretCommand
|
|
103
|
+
.command('create')
|
|
104
|
+
.description('create a secret in AWS Secrets Manager')
|
|
105
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
106
|
+
.option('-v, --value <value>', 'secret value')
|
|
107
|
+
.option('--value-stdin', 'read secret value from stdin')
|
|
108
|
+
.option('-f, --file <path>', 'read secret value from local file')
|
|
109
|
+
.option('-d, --description <description>', 'secret description')
|
|
110
|
+
.option('-k, --kms-key-id <kmsKeyId>', 'kms key id')
|
|
111
|
+
.option('-t, --tag <tag...>', 'tag in key=value format')
|
|
112
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
113
|
+
.option('-r, --region <region>', 'region to use')
|
|
114
|
+
.option('--output <format>', 'output format: json|table')
|
|
115
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
|
+
var _a;
|
|
117
|
+
try {
|
|
118
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
119
|
+
const globalOptions = command.optsWithGlobals();
|
|
120
|
+
const output = (_a = options.output) !== null && _a !== void 0 ? _a : (typeof globalOptions.output === 'string'
|
|
121
|
+
? globalOptions.output
|
|
122
|
+
: 'table');
|
|
123
|
+
const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin, options.file);
|
|
124
|
+
if (!value) {
|
|
125
|
+
throw new Error('Secret value is required. Provide --value, --value-stdin, or --file.');
|
|
126
|
+
}
|
|
127
|
+
const result = yield (0, secretsmanager_admin_1.createSecret)({
|
|
128
|
+
name: options.name,
|
|
129
|
+
value,
|
|
130
|
+
description: options.description,
|
|
131
|
+
kmsKeyId: options.kmsKeyId,
|
|
132
|
+
tags: options.tag,
|
|
133
|
+
profile,
|
|
134
|
+
region
|
|
135
|
+
});
|
|
136
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
137
|
+
{ key: 'name', label: 'Name' },
|
|
138
|
+
{ key: 'arn', label: 'ARN' },
|
|
139
|
+
{ key: 'versionId', label: 'VersionId' }
|
|
140
|
+
], [result]);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
exitWithError(error);
|
|
144
|
+
}
|
|
145
|
+
}));
|
|
146
|
+
secretCommand
|
|
147
|
+
.command('update')
|
|
148
|
+
.description('update secret value or metadata')
|
|
149
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
150
|
+
.option('-v, --value <value>', 'new secret value')
|
|
151
|
+
.option('--value-stdin', 'read secret value from stdin')
|
|
152
|
+
.option('-f, --file <path>', 'read secret value from local file')
|
|
153
|
+
.option('-d, --description <description>', 'secret description')
|
|
154
|
+
.option('-k, --kms-key-id <kmsKeyId>', 'kms key id')
|
|
155
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
156
|
+
.option('-r, --region <region>', 'region to use')
|
|
157
|
+
.option('--output <format>', 'output format: json|table')
|
|
158
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
159
|
+
var _b;
|
|
160
|
+
try {
|
|
161
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
162
|
+
const globalOptions = command.optsWithGlobals();
|
|
163
|
+
const output = (_b = options.output) !== null && _b !== void 0 ? _b : (typeof globalOptions.output === 'string'
|
|
164
|
+
? globalOptions.output
|
|
165
|
+
: 'table');
|
|
166
|
+
const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin, options.file);
|
|
167
|
+
if (!value && !options.description && !options.kmsKeyId) {
|
|
168
|
+
throw new Error('Nothing to update. Provide --value/--value-stdin/--file, --description, or --kms-key-id.');
|
|
169
|
+
}
|
|
170
|
+
const result = yield (0, secretsmanager_admin_1.updateSecret)({
|
|
171
|
+
name: options.name,
|
|
172
|
+
value,
|
|
173
|
+
description: options.description,
|
|
174
|
+
kmsKeyId: options.kmsKeyId,
|
|
175
|
+
profile,
|
|
176
|
+
region
|
|
177
|
+
});
|
|
178
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
179
|
+
{ key: 'name', label: 'Name' },
|
|
180
|
+
{ key: 'arn', label: 'ARN' },
|
|
181
|
+
{ key: 'versionId', label: 'VersionId' }
|
|
182
|
+
], [result]);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
exitWithError(error);
|
|
186
|
+
}
|
|
187
|
+
}));
|
|
188
|
+
secretCommand
|
|
189
|
+
.command('upsert')
|
|
190
|
+
.alias('import')
|
|
191
|
+
.description('create or update a secret from a local env file')
|
|
192
|
+
.requiredOption('-f, --file <path>', 'path to env file')
|
|
193
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
194
|
+
.option('-d, --description <description>', 'secret description')
|
|
195
|
+
.option('-k, --kms-key-id <kmsKeyId>', 'kms key id')
|
|
196
|
+
.option('-t, --tag <tag...>', 'tag in key=value format (applies on create)')
|
|
197
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
198
|
+
.option('-r, --region <region>', 'region to use')
|
|
199
|
+
.option('--output <format>', 'output format: json|table')
|
|
200
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
201
|
+
var _c;
|
|
202
|
+
try {
|
|
203
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
204
|
+
const globalOptions = command.optsWithGlobals();
|
|
205
|
+
const output = (_c = options.output) !== null && _c !== void 0 ? _c : (typeof globalOptions.output === 'string'
|
|
206
|
+
? globalOptions.output
|
|
207
|
+
: 'table');
|
|
208
|
+
const parsed = yield (0, helpers_1.parseEnvSecretsFile)(options.file);
|
|
209
|
+
if (parsed.entries.length === 0) {
|
|
210
|
+
throw new Error('No env entries found. Include lines like KEY=value or export KEY=value.');
|
|
211
|
+
}
|
|
212
|
+
const payload = Object.fromEntries(parsed.entries.map((entry) => [entry.key, entry.value]));
|
|
213
|
+
const value = JSON.stringify(payload);
|
|
214
|
+
const rows = [];
|
|
215
|
+
let created = 0;
|
|
216
|
+
let updated = 0;
|
|
217
|
+
let failed = 0;
|
|
218
|
+
const skipped = parsed.skipped.length;
|
|
219
|
+
for (const skip of parsed.skipped) {
|
|
220
|
+
rows.push({
|
|
221
|
+
name: options.name,
|
|
222
|
+
status: 'skipped',
|
|
223
|
+
line: String(skip.line),
|
|
224
|
+
message: `${skip.reason}: ${skip.key}`
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
try {
|
|
229
|
+
yield (0, secretsmanager_admin_1.createSecret)({
|
|
230
|
+
name: options.name,
|
|
231
|
+
value,
|
|
232
|
+
description: options.description,
|
|
233
|
+
kmsKeyId: options.kmsKeyId,
|
|
234
|
+
tags: options.tag,
|
|
235
|
+
profile,
|
|
236
|
+
region
|
|
237
|
+
});
|
|
238
|
+
created += 1;
|
|
239
|
+
rows.push({
|
|
240
|
+
name: options.name,
|
|
241
|
+
status: 'created',
|
|
242
|
+
message: `imported ${parsed.entries.length} keys`
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (createError) {
|
|
246
|
+
const message = createError instanceof Error
|
|
247
|
+
? createError.message
|
|
248
|
+
: String(createError);
|
|
249
|
+
if (!/already exists/i.test(message)) {
|
|
250
|
+
throw createError;
|
|
251
|
+
}
|
|
252
|
+
yield (0, secretsmanager_admin_1.updateSecret)({
|
|
253
|
+
name: options.name,
|
|
254
|
+
value,
|
|
255
|
+
description: options.description,
|
|
256
|
+
kmsKeyId: options.kmsKeyId,
|
|
257
|
+
profile,
|
|
258
|
+
region
|
|
259
|
+
});
|
|
260
|
+
updated += 1;
|
|
261
|
+
rows.push({
|
|
262
|
+
name: options.name,
|
|
263
|
+
status: 'updated',
|
|
264
|
+
message: `imported ${parsed.entries.length} keys`
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
failed += 1;
|
|
270
|
+
rows.push({
|
|
271
|
+
name: options.name,
|
|
272
|
+
status: 'failed',
|
|
273
|
+
message: error instanceof Error ? error.message : String(error)
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
const summary = { created, updated, skipped, failed };
|
|
277
|
+
if (output === 'json') {
|
|
278
|
+
// eslint-disable-next-line no-console
|
|
279
|
+
console.log(JSON.stringify({ summary, results: rows }, null, 2));
|
|
280
|
+
if (failed > 0) {
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
286
|
+
{ key: 'name', label: 'Name' },
|
|
287
|
+
{ key: 'status', label: 'Status' },
|
|
288
|
+
{ key: 'line', label: 'Line' },
|
|
289
|
+
{ key: 'message', label: 'Message' }
|
|
290
|
+
], rows);
|
|
291
|
+
// eslint-disable-next-line no-console
|
|
292
|
+
console.log(`Summary: created=${created}, updated=${updated}, skipped=${skipped}, failed=${failed}`);
|
|
293
|
+
if (failed > 0) {
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
exitWithError(error);
|
|
299
|
+
}
|
|
300
|
+
}));
|
|
301
|
+
secretCommand
|
|
302
|
+
.command('append')
|
|
303
|
+
.description('append or overwrite one key in an existing JSON secret')
|
|
304
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
305
|
+
.requiredOption('--key <key>', 'key to append/update')
|
|
306
|
+
.option('-v, --value <value>', 'value for the key')
|
|
307
|
+
.option('--value-stdin', 'read value from stdin')
|
|
308
|
+
.option('-f, --file <path>', 'read value from local file')
|
|
309
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
310
|
+
.option('-r, --region <region>', 'region to use')
|
|
311
|
+
.option('--output <format>', 'output format: json|table')
|
|
312
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
313
|
+
var _d;
|
|
314
|
+
try {
|
|
315
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
316
|
+
const globalOptions = command.optsWithGlobals();
|
|
317
|
+
const output = (_d = options.output) !== null && _d !== void 0 ? _d : (typeof globalOptions.output === 'string'
|
|
318
|
+
? globalOptions.output
|
|
319
|
+
: 'table');
|
|
320
|
+
const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin, options.file);
|
|
321
|
+
if (!value) {
|
|
322
|
+
throw new Error('Append value is required. Provide --value, --value-stdin, or --file.');
|
|
323
|
+
}
|
|
324
|
+
const current = yield (0, secretsmanager_admin_1.getSecretString)({
|
|
325
|
+
name: options.name,
|
|
326
|
+
profile,
|
|
327
|
+
region
|
|
328
|
+
});
|
|
329
|
+
const payload = parseSecretJsonObject(options.name, current);
|
|
330
|
+
payload[options.key] = value;
|
|
331
|
+
const result = yield (0, secretsmanager_admin_1.updateSecret)({
|
|
332
|
+
name: options.name,
|
|
333
|
+
value: JSON.stringify(payload),
|
|
334
|
+
profile,
|
|
335
|
+
region
|
|
336
|
+
});
|
|
337
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
338
|
+
{ key: 'name', label: 'Name' },
|
|
339
|
+
{ key: 'arn', label: 'ARN' },
|
|
340
|
+
{ key: 'versionId', label: 'VersionId' },
|
|
341
|
+
{ key: 'key', label: 'Key' },
|
|
342
|
+
{ key: 'action', label: 'Action' }
|
|
343
|
+
], [
|
|
344
|
+
Object.assign(Object.assign({}, result), { key: options.key, action: 'appended' })
|
|
345
|
+
]);
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
exitWithError(error);
|
|
349
|
+
}
|
|
350
|
+
}));
|
|
351
|
+
secretCommand
|
|
352
|
+
.command('remove')
|
|
353
|
+
.description('remove one or more keys from an existing JSON secret')
|
|
354
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
355
|
+
.requiredOption('--key <key...>', 'one or more keys to remove')
|
|
356
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
357
|
+
.option('-r, --region <region>', 'region to use')
|
|
358
|
+
.option('--output <format>', 'output format: json|table')
|
|
359
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
360
|
+
var _e;
|
|
361
|
+
try {
|
|
362
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
363
|
+
const globalOptions = command.optsWithGlobals();
|
|
364
|
+
const output = (_e = options.output) !== null && _e !== void 0 ? _e : (typeof globalOptions.output === 'string'
|
|
365
|
+
? globalOptions.output
|
|
366
|
+
: 'table');
|
|
367
|
+
const keys = options.key;
|
|
368
|
+
const current = yield (0, secretsmanager_admin_1.getSecretString)({
|
|
369
|
+
name: options.name,
|
|
370
|
+
profile,
|
|
371
|
+
region
|
|
372
|
+
});
|
|
373
|
+
const payload = parseSecretJsonObject(options.name, current);
|
|
374
|
+
const removed = [];
|
|
375
|
+
const missing = [];
|
|
376
|
+
for (const key of keys) {
|
|
377
|
+
if (Object.prototype.hasOwnProperty.call(payload, key)) {
|
|
378
|
+
delete payload[key];
|
|
379
|
+
removed.push(key);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
missing.push(key);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (removed.length === 0) {
|
|
386
|
+
throw new Error(`None of the requested keys exist in secret "${options.name}".`);
|
|
387
|
+
}
|
|
388
|
+
const result = yield (0, secretsmanager_admin_1.updateSecret)({
|
|
389
|
+
name: options.name,
|
|
390
|
+
value: JSON.stringify(payload),
|
|
391
|
+
profile,
|
|
392
|
+
region
|
|
393
|
+
});
|
|
394
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
395
|
+
{ key: 'name', label: 'Name' },
|
|
396
|
+
{ key: 'arn', label: 'ARN' },
|
|
397
|
+
{ key: 'versionId', label: 'VersionId' },
|
|
398
|
+
{ key: 'removed', label: 'RemovedKeys' },
|
|
399
|
+
{ key: 'missing', label: 'MissingKeys' }
|
|
400
|
+
], [
|
|
401
|
+
Object.assign(Object.assign({}, result), { removed: removed.join(','), missing: missing.join(',') })
|
|
402
|
+
]);
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
exitWithError(error);
|
|
406
|
+
}
|
|
407
|
+
}));
|
|
408
|
+
secretCommand
|
|
409
|
+
.command('list')
|
|
410
|
+
.description('list secrets in AWS Secrets Manager')
|
|
411
|
+
.option('--prefix <prefix>', 'filter secrets by name prefix')
|
|
412
|
+
.option('-t, --tag <tag...>', 'filter tags in key=value format')
|
|
413
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
414
|
+
.option('-r, --region <region>', 'region to use')
|
|
415
|
+
.option('--output <format>', 'output format: json|table')
|
|
416
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
417
|
+
var _f;
|
|
418
|
+
try {
|
|
419
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
420
|
+
const globalOptions = command.optsWithGlobals();
|
|
421
|
+
const output = (_f = options.output) !== null && _f !== void 0 ? _f : (typeof globalOptions.output === 'string'
|
|
422
|
+
? globalOptions.output
|
|
423
|
+
: 'table');
|
|
424
|
+
const result = yield (0, secretsmanager_admin_1.listSecrets)({
|
|
425
|
+
prefix: options.prefix,
|
|
426
|
+
tags: options.tag,
|
|
427
|
+
profile,
|
|
428
|
+
region
|
|
429
|
+
});
|
|
430
|
+
const rows = result.map((secret) => ({
|
|
431
|
+
name: secret.name,
|
|
432
|
+
description: secret.description,
|
|
433
|
+
lastChangedDate: secret.lastChangedDate
|
|
434
|
+
}));
|
|
435
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
436
|
+
{ key: 'name', label: 'Name' },
|
|
437
|
+
{ key: 'description', label: 'Description' },
|
|
438
|
+
{ key: 'lastChangedDate', label: 'LastChanged' }
|
|
439
|
+
], rows);
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
exitWithError(error);
|
|
443
|
+
}
|
|
444
|
+
}));
|
|
445
|
+
secretCommand
|
|
446
|
+
.command('get')
|
|
447
|
+
.description('get secret metadata and version information')
|
|
448
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
449
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
450
|
+
.option('-r, --region <region>', 'region to use')
|
|
451
|
+
.option('--output <format>', 'output format: json|table')
|
|
452
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
453
|
+
var _g;
|
|
454
|
+
try {
|
|
455
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
456
|
+
const globalOptions = command.optsWithGlobals();
|
|
457
|
+
const output = (_g = options.output) !== null && _g !== void 0 ? _g : (typeof globalOptions.output === 'string'
|
|
458
|
+
? globalOptions.output
|
|
459
|
+
: 'table');
|
|
460
|
+
const result = yield (0, secretsmanager_admin_1.getSecretMetadata)({
|
|
461
|
+
name: options.name,
|
|
462
|
+
profile,
|
|
463
|
+
region
|
|
464
|
+
});
|
|
465
|
+
const row = {
|
|
466
|
+
name: result.name,
|
|
467
|
+
arn: result.arn,
|
|
468
|
+
description: result.description,
|
|
469
|
+
kmsKeyId: result.kmsKeyId,
|
|
470
|
+
createdDate: result.createdDate,
|
|
471
|
+
lastChangedDate: result.lastChangedDate,
|
|
472
|
+
deletedDate: result.deletedDate
|
|
473
|
+
};
|
|
474
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
475
|
+
{ key: 'name', label: 'Name' },
|
|
476
|
+
{ key: 'arn', label: 'ARN' },
|
|
477
|
+
{ key: 'description', label: 'Description' },
|
|
478
|
+
{ key: 'kmsKeyId', label: 'KmsKeyId' },
|
|
479
|
+
{ key: 'createdDate', label: 'Created' },
|
|
480
|
+
{ key: 'lastChangedDate', label: 'LastChanged' },
|
|
481
|
+
{ key: 'deletedDate', label: 'Deleted' }
|
|
482
|
+
], [row]);
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
exitWithError(error);
|
|
486
|
+
}
|
|
487
|
+
}));
|
|
488
|
+
secretCommand
|
|
489
|
+
.command('delete')
|
|
490
|
+
.description('delete a secret in AWS Secrets Manager')
|
|
491
|
+
.requiredOption('-n, --name <name>', 'secret name')
|
|
492
|
+
.option('--recovery-days <days>', 'recovery window in days (7-30)', helpers_1.parseRecoveryDays)
|
|
493
|
+
.option('--force-delete-without-recovery', 'permanently delete secret without recovery window', false)
|
|
494
|
+
.option('-y, --yes', 'confirm delete action', false)
|
|
495
|
+
.option('-p, --profile <profile>', 'profile to use')
|
|
496
|
+
.option('-r, --region <region>', 'region to use')
|
|
497
|
+
.option('--output <format>', 'output format: json|table')
|
|
498
|
+
.action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
|
|
499
|
+
var _h;
|
|
500
|
+
try {
|
|
501
|
+
const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
|
|
502
|
+
const globalOptions = command.optsWithGlobals();
|
|
503
|
+
const output = (_h = options.output) !== null && _h !== void 0 ? _h : (typeof globalOptions.output === 'string'
|
|
504
|
+
? globalOptions.output
|
|
505
|
+
: 'table');
|
|
506
|
+
if (!options.yes) {
|
|
507
|
+
throw new Error('Delete requires --yes confirmation.');
|
|
508
|
+
}
|
|
509
|
+
if (options.recoveryDays && options.forceDeleteWithoutRecovery) {
|
|
510
|
+
throw new Error('Use either --recovery-days or --force-delete-without-recovery, not both.');
|
|
511
|
+
}
|
|
512
|
+
const result = yield (0, secretsmanager_admin_1.deleteSecret)({
|
|
513
|
+
name: options.name,
|
|
514
|
+
recoveryDays: options.recoveryDays,
|
|
515
|
+
forceDeleteWithoutRecovery: options.forceDeleteWithoutRecovery,
|
|
516
|
+
profile,
|
|
517
|
+
region
|
|
518
|
+
});
|
|
519
|
+
(0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
|
|
520
|
+
{ key: 'name', label: 'Name' },
|
|
521
|
+
{ key: 'arn', label: 'ARN' },
|
|
522
|
+
{ key: 'deletedDate', label: 'DeletedDate' }
|
|
523
|
+
], [result]);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
exitWithError(error);
|
|
527
|
+
}
|
|
528
|
+
}));
|
|
75
529
|
program.parse();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAwsClientConfig = void 0;
|
|
4
|
+
const credential_providers_1 = require("@aws-sdk/credential-providers");
|
|
5
|
+
const getCredentialsProvider = (options) => {
|
|
6
|
+
const { AWS_ACCESS_KEY_ID: awsAccessKeyId, AWS_SECRET_ACCESS_KEY: awsSecretAccessKey } = process.env;
|
|
7
|
+
if (options.profile) {
|
|
8
|
+
return (0, credential_providers_1.fromIni)({ profile: options.profile });
|
|
9
|
+
}
|
|
10
|
+
if (awsAccessKeyId && awsSecretAccessKey) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return (0, credential_providers_1.fromIni)({ profile: 'default' });
|
|
14
|
+
};
|
|
15
|
+
const getEndpoint = () => {
|
|
16
|
+
return (process.env.AWS_ENDPOINT_URL || process.env.AWS_SECRETS_MANAGER_ENDPOINT);
|
|
17
|
+
};
|
|
18
|
+
const buildAwsClientConfig = (options) => {
|
|
19
|
+
const endpoint = getEndpoint();
|
|
20
|
+
const config = {
|
|
21
|
+
region: options.region,
|
|
22
|
+
credentials: getCredentialsProvider(options)
|
|
23
|
+
};
|
|
24
|
+
if (endpoint) {
|
|
25
|
+
config.endpoint = endpoint;
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
};
|
|
29
|
+
exports.buildAwsClientConfig = buildAwsClientConfig;
|