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.
@@ -0,0 +1,273 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.deleteSecret = exports.getSecretString = exports.secretExists = exports.getSecretMetadata = exports.listSecrets = exports.updateSecret = exports.createSecret = exports.validateSecretName = void 0;
16
+ const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
17
+ const client_sts_1 = require("@aws-sdk/client-sts");
18
+ const debug_1 = __importDefault(require("debug"));
19
+ const aws_config_1 = require("./aws-config");
20
+ const debug = (0, debug_1.default)('env-secrets:secretsmanager-admin');
21
+ // Allowed characters are documented by AWS Secrets Manager naming rules.
22
+ // See: https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_limits.html
23
+ const SECRET_NAME_PATTERN = /^[A-Za-z0-9/_+=.@-]+$/;
24
+ const formatDate = (value) => {
25
+ if (!value) {
26
+ return undefined;
27
+ }
28
+ return value.toISOString();
29
+ };
30
+ const parseTags = (tags) => {
31
+ if (!tags || tags.length === 0) {
32
+ return undefined;
33
+ }
34
+ return tags.map((tag) => {
35
+ const parts = tag.split('=');
36
+ if (parts.length < 2) {
37
+ throw new Error(`Invalid tag format: ${tag}. Use key=value.`);
38
+ }
39
+ const key = parts[0].trim();
40
+ const value = parts.slice(1).join('=').trim();
41
+ if (!key || !value) {
42
+ throw new Error(`Invalid tag format: ${tag}. Use key=value.`);
43
+ }
44
+ return { Key: key, Value: value };
45
+ });
46
+ };
47
+ const tagsToRecord = (tags) => {
48
+ if (!tags || tags.length === 0) {
49
+ return undefined;
50
+ }
51
+ const result = {};
52
+ for (const tag of tags) {
53
+ if (tag.Key && tag.Value) {
54
+ result[tag.Key] = tag.Value;
55
+ }
56
+ }
57
+ return Object.keys(result).length > 0 ? result : undefined;
58
+ };
59
+ const mapAwsError = (error, secretName) => {
60
+ const awsError = error;
61
+ const secretLabel = secretName ? ` for "${secretName}"` : '';
62
+ if ((awsError === null || awsError === void 0 ? void 0 : awsError.name) === 'AlreadyExistsException') {
63
+ throw new Error(`Secret${secretLabel} already exists.`);
64
+ }
65
+ if ((awsError === null || awsError === void 0 ? void 0 : awsError.name) === 'ResourceNotFoundException') {
66
+ throw new Error(`Secret${secretLabel} was not found.`);
67
+ }
68
+ if ((awsError === null || awsError === void 0 ? void 0 : awsError.name) === 'InvalidRequestException') {
69
+ throw new Error(awsError.message || 'Invalid request to AWS Secrets Manager.');
70
+ }
71
+ if ((awsError === null || awsError === void 0 ? void 0 : awsError.name) === 'AccessDeniedException') {
72
+ throw new Error(awsError.message ||
73
+ 'Access denied while calling AWS Secrets Manager. Verify IAM permissions.');
74
+ }
75
+ if (awsError === null || awsError === void 0 ? void 0 : awsError.message) {
76
+ throw new Error(awsError.message);
77
+ }
78
+ throw new Error(String(error));
79
+ };
80
+ const ensureConnected = (clientConfig) => __awaiter(void 0, void 0, void 0, function* () {
81
+ const stsClient = new client_sts_1.STSClient(clientConfig);
82
+ yield stsClient.send(new client_sts_1.GetCallerIdentityCommand({}));
83
+ });
84
+ const createClient = (options) => __awaiter(void 0, void 0, void 0, function* () {
85
+ const config = (0, aws_config_1.buildAwsClientConfig)(options);
86
+ debug('Creating AWS clients', {
87
+ hasProfile: Boolean(options.profile),
88
+ region: options.region,
89
+ hasEndpoint: Boolean(config.endpoint)
90
+ });
91
+ yield ensureConnected(config);
92
+ return new client_secrets_manager_1.SecretsManagerClient(config);
93
+ });
94
+ const validateSecretName = (name) => {
95
+ if (!SECRET_NAME_PATTERN.test(name)) {
96
+ throw new Error(`Invalid secret name "${name}". Use only letters, numbers, and /_+=.@- characters.`);
97
+ }
98
+ };
99
+ exports.validateSecretName = validateSecretName;
100
+ const createSecret = (options) => __awaiter(void 0, void 0, void 0, function* () {
101
+ var _a;
102
+ (0, exports.validateSecretName)(options.name);
103
+ debug('createSecret called', {
104
+ name: options.name,
105
+ hasTags: !!((_a = options.tags) === null || _a === void 0 ? void 0 : _a.length)
106
+ });
107
+ const client = yield createClient(options);
108
+ const tags = parseTags(options.tags);
109
+ try {
110
+ const result = yield client.send(new client_secrets_manager_1.CreateSecretCommand({
111
+ Name: options.name,
112
+ Description: options.description,
113
+ SecretString: options.value,
114
+ KmsKeyId: options.kmsKeyId,
115
+ Tags: tags
116
+ }));
117
+ return {
118
+ arn: result.ARN,
119
+ name: result.Name,
120
+ versionId: result.VersionId
121
+ };
122
+ }
123
+ catch (error) {
124
+ return mapAwsError(error, options.name);
125
+ }
126
+ });
127
+ exports.createSecret = createSecret;
128
+ const updateSecret = (options) => __awaiter(void 0, void 0, void 0, function* () {
129
+ (0, exports.validateSecretName)(options.name);
130
+ debug('updateSecret called', { name: options.name });
131
+ const client = yield createClient(options);
132
+ try {
133
+ const result = yield client.send(new client_secrets_manager_1.UpdateSecretCommand({
134
+ SecretId: options.name,
135
+ Description: options.description,
136
+ SecretString: options.value,
137
+ KmsKeyId: options.kmsKeyId
138
+ }));
139
+ return {
140
+ arn: result.ARN,
141
+ name: result.Name,
142
+ versionId: result.VersionId
143
+ };
144
+ }
145
+ catch (error) {
146
+ return mapAwsError(error, options.name);
147
+ }
148
+ });
149
+ exports.updateSecret = updateSecret;
150
+ const listSecrets = (options) => __awaiter(void 0, void 0, void 0, function* () {
151
+ var _b;
152
+ debug('listSecrets called', {
153
+ prefix: options.prefix,
154
+ hasTags: !!((_b = options.tags) === null || _b === void 0 ? void 0 : _b.length)
155
+ });
156
+ const client = yield createClient(options);
157
+ const requiredTags = parseTags(options.tags);
158
+ const secrets = [];
159
+ try {
160
+ let nextToken;
161
+ do {
162
+ const result = yield client.send(new client_secrets_manager_1.ListSecretsCommand({ NextToken: nextToken }));
163
+ for (const secret of result.SecretList || []) {
164
+ if (options.prefix &&
165
+ secret.Name &&
166
+ !secret.Name.startsWith(options.prefix)) {
167
+ continue;
168
+ }
169
+ if (requiredTags && requiredTags.length > 0) {
170
+ const available = tagsToRecord(secret.Tags);
171
+ const matchesAll = requiredTags.every((tag) => tag.Key && tag.Value && (available === null || available === void 0 ? void 0 : available[tag.Key]) === tag.Value);
172
+ if (!matchesAll) {
173
+ continue;
174
+ }
175
+ }
176
+ secrets.push({
177
+ name: secret.Name || '',
178
+ arn: secret.ARN,
179
+ description: secret.Description,
180
+ lastChangedDate: formatDate(secret.LastChangedDate)
181
+ });
182
+ }
183
+ nextToken = result.NextToken;
184
+ } while (nextToken);
185
+ }
186
+ catch (error) {
187
+ return mapAwsError(error);
188
+ }
189
+ return secrets;
190
+ });
191
+ exports.listSecrets = listSecrets;
192
+ const getSecretMetadata = (options) => __awaiter(void 0, void 0, void 0, function* () {
193
+ (0, exports.validateSecretName)(options.name);
194
+ debug('getSecretMetadata called', { name: options.name });
195
+ const client = yield createClient(options);
196
+ try {
197
+ const result = yield client.send(new client_secrets_manager_1.DescribeSecretCommand({ SecretId: options.name }));
198
+ return {
199
+ name: result.Name,
200
+ arn: result.ARN,
201
+ description: result.Description,
202
+ kmsKeyId: result.KmsKeyId,
203
+ deletedDate: formatDate(result.DeletedDate),
204
+ lastChangedDate: formatDate(result.LastChangedDate),
205
+ lastAccessedDate: formatDate(result.LastAccessedDate),
206
+ createdDate: formatDate(result.CreatedDate),
207
+ versionIdsToStages: result.VersionIdsToStages,
208
+ tags: tagsToRecord(result.Tags)
209
+ };
210
+ }
211
+ catch (error) {
212
+ return mapAwsError(error, options.name);
213
+ }
214
+ });
215
+ exports.getSecretMetadata = getSecretMetadata;
216
+ const secretExists = (options) => __awaiter(void 0, void 0, void 0, function* () {
217
+ (0, exports.validateSecretName)(options.name);
218
+ debug('secretExists called', { name: options.name });
219
+ const client = yield createClient(options);
220
+ try {
221
+ yield client.send(new client_secrets_manager_1.DescribeSecretCommand({ SecretId: options.name }));
222
+ return true;
223
+ }
224
+ catch (error) {
225
+ const awsError = error;
226
+ if ((awsError === null || awsError === void 0 ? void 0 : awsError.name) === 'ResourceNotFoundException') {
227
+ return false;
228
+ }
229
+ return mapAwsError(error, options.name);
230
+ }
231
+ });
232
+ exports.secretExists = secretExists;
233
+ const getSecretString = (options) => __awaiter(void 0, void 0, void 0, function* () {
234
+ (0, exports.validateSecretName)(options.name);
235
+ debug('getSecretString called', { name: options.name });
236
+ const client = yield createClient(options);
237
+ try {
238
+ const result = yield client.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: options.name }));
239
+ if (typeof result.SecretString !== 'string') {
240
+ throw new Error(`Secret "${options.name}" is not stored as a string value and cannot be edited with append/remove.`);
241
+ }
242
+ return result.SecretString;
243
+ }
244
+ catch (error) {
245
+ return mapAwsError(error, options.name);
246
+ }
247
+ });
248
+ exports.getSecretString = getSecretString;
249
+ const deleteSecret = (options) => __awaiter(void 0, void 0, void 0, function* () {
250
+ (0, exports.validateSecretName)(options.name);
251
+ debug('deleteSecret called', {
252
+ name: options.name,
253
+ recoveryDays: options.recoveryDays,
254
+ forceDeleteWithoutRecovery: options.forceDeleteWithoutRecovery
255
+ });
256
+ const client = yield createClient(options);
257
+ try {
258
+ const result = yield client.send(new client_secrets_manager_1.DeleteSecretCommand({
259
+ SecretId: options.name,
260
+ RecoveryWindowInDays: options.recoveryDays,
261
+ ForceDeleteWithoutRecovery: options.forceDeleteWithoutRecovery
262
+ }));
263
+ return {
264
+ arn: result.ARN,
265
+ name: result.Name,
266
+ deletedDate: formatDate(result.DeletionDate)
267
+ };
268
+ }
269
+ catch (error) {
270
+ return mapAwsError(error, options.name);
271
+ }
272
+ });
273
+ exports.deleteSecret = deleteSecret;
@@ -15,8 +15,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.secretsmanager = void 0;
16
16
  const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
17
17
  const client_sts_1 = require("@aws-sdk/client-sts");
18
- const credential_providers_1 = require("@aws-sdk/credential-providers");
19
18
  const debug_1 = __importDefault(require("debug"));
19
+ const aws_config_1 = require("./aws-config");
20
20
  const debug = (0, debug_1.default)('env-secrets:secretsmanager');
21
21
  const isCredentialsError = (error) => {
22
22
  if (!error || typeof error !== 'object') {
@@ -25,8 +25,8 @@ const isCredentialsError = (error) => {
25
25
  const errorName = 'name' in error ? error.name : undefined;
26
26
  return (errorName === 'CredentialsError' || errorName === 'CredentialsProviderError');
27
27
  };
28
- const checkConnection = (region, credentials) => __awaiter(void 0, void 0, void 0, function* () {
29
- const stsClient = new client_sts_1.STSClient({ region, credentials });
28
+ const checkConnection = (config) => __awaiter(void 0, void 0, void 0, function* () {
29
+ const stsClient = new client_sts_1.STSClient(config);
30
30
  const command = new client_sts_1.GetCallerIdentityCommand({});
31
31
  try {
32
32
  const data = yield stsClient.send(command);
@@ -46,28 +46,20 @@ const checkConnection = (region, credentials) => __awaiter(void 0, void 0, void
46
46
  });
47
47
  const secretsmanager = (options) => __awaiter(void 0, void 0, void 0, function* () {
48
48
  const { secret, profile, region } = options;
49
- const { AWS_ACCESS_KEY_ID: awsAccessKeyId, AWS_SECRET_ACCESS_KEY: awsSecretAccessKey } = process.env;
50
- let credentials;
49
+ const config = (0, aws_config_1.buildAwsClientConfig)({ profile, region });
51
50
  if (profile) {
52
51
  debug(`Using profile: ${profile}`);
53
- credentials = (0, credential_providers_1.fromIni)({ profile });
54
52
  }
55
- else if (awsAccessKeyId && awsSecretAccessKey) {
56
- debug('Using environment variables');
57
- credentials = undefined; // Will use environment variables automatically
53
+ else if (config.credentials) {
54
+ debug('Using profile: default');
58
55
  }
59
56
  else {
60
- debug('Using profile: default');
61
- credentials = (0, credential_providers_1.fromIni)({ profile: 'default' });
57
+ debug('Using environment variables');
62
58
  }
63
- const config = {
64
- region,
65
- credentials
66
- };
67
59
  if (!config.region) {
68
60
  debug('no region set');
69
61
  }
70
- const connected = yield checkConnection(region, credentials);
62
+ const connected = yield checkConnection(config);
71
63
  if (connected) {
72
64
  const client = new client_secrets_manager_1.SecretsManagerClient(config);
73
65
  try {
package/docs/AWS.md CHANGED
@@ -120,7 +120,7 @@ env-secrets aws -s my-secret-name -r us-east-1 -- node app.js
120
120
 
121
121
  ## Required Permissions
122
122
 
123
- Your AWS credentials must have the following permissions to use Secrets Manager:
123
+ Your AWS credentials must have the following permissions to use secret injection and secret management commands:
124
124
 
125
125
  ```json
126
126
  {
@@ -128,7 +128,14 @@ Your AWS credentials must have the following permissions to use Secrets Manager:
128
128
  "Statement": [
129
129
  {
130
130
  "Effect": "Allow",
131
- "Action": ["secretsmanager:GetSecretValue"],
131
+ "Action": [
132
+ "secretsmanager:GetSecretValue",
133
+ "secretsmanager:CreateSecret",
134
+ "secretsmanager:UpdateSecret",
135
+ "secretsmanager:ListSecrets",
136
+ "secretsmanager:DescribeSecret",
137
+ "secretsmanager:DeleteSecret"
138
+ ],
132
139
  "Resource": "arn:aws:secretsmanager:*:*:secret:*"
133
140
  },
134
141
  {
@@ -140,6 +147,126 @@ Your AWS credentials must have the following permissions to use Secrets Manager:
140
147
  }
141
148
  ```
142
149
 
150
+ ## Secret Management Commands
151
+
152
+ In addition to injecting variables into a process, `env-secrets` can manage AWS secrets directly:
153
+
154
+ - `env-secrets aws secret create`
155
+ - `env-secrets aws secret update`
156
+ - `env-secrets aws secret append`
157
+ - `env-secrets aws secret remove`
158
+ - `env-secrets aws secret upsert` (alias: `import`)
159
+ - `env-secrets aws secret list`
160
+ - `env-secrets aws secret get`
161
+ - `env-secrets aws secret delete`
162
+
163
+ `aws secret` subcommands consistently honor `--region`, `--profile`, and `--output`.
164
+ Use these options directly with each subcommand.
165
+
166
+ ### `aws -s` vs `aws secret ...`
167
+
168
+ - `env-secrets aws -s <secret-name> -- <command>`: retrieves a secret value and injects it into the environment for the spawned process (or use `-o <file>` to write exports to a file).
169
+ - `env-secrets aws secret ...`: management commands only (`create`, `update`, `append`, `remove`, `upsert/import`, `list`, `get`, `delete`).
170
+
171
+ Example:
172
+
173
+ ```bash
174
+ # inject secret values
175
+ env-secrets aws -s my-app/dev/api -r us-east-1 -- node app.js
176
+
177
+ # manage secrets
178
+ env-secrets aws secret get -n my-app/dev/api -r us-east-1 --output json
179
+ ```
180
+
181
+ ### Load secrets into your current shell
182
+
183
+ `env-secrets aws -s ... -- <command>` injects variables into the spawned child process only.
184
+ If you want variables in your current shell session, write exports to a file and source it:
185
+
186
+ ```bash
187
+ env-secrets aws -s my-app/dev/api -r us-east-1 -o secrets.env
188
+ source secrets.env
189
+ ```
190
+
191
+ ### Secret Management Examples
192
+
193
+ 1. **Create a secret with inline value:**
194
+
195
+ ```bash
196
+ env-secrets aws secret create \
197
+ -n my-app/dev/api \
198
+ -v '{"API_KEY":"abc123"}' \
199
+ -r us-east-1 \
200
+ --output json
201
+ ```
202
+
203
+ 2. **Create from stdin (recommended for sensitive values):**
204
+
205
+ ```bash
206
+ echo -n 'super-secret-value' | env-secrets aws secret create -n my-app/dev/raw --value-stdin -r us-east-1
207
+ ```
208
+
209
+ 3. **Update an existing secret value:**
210
+
211
+ ```bash
212
+ env-secrets aws secret update -n my-app/dev/api -v '{"API_KEY":"rotated"}' -r us-east-1
213
+ ```
214
+
215
+ 4. **Upsert from an env file into one JSON secret (`export KEY=value` or `KEY=value`):**
216
+
217
+ ```bash
218
+ env-secrets aws secret upsert --file .env --name my-app/dev -r us-east-1 --output json
219
+ # alias:
220
+ env-secrets aws secret import --file .env --name my-app/dev -r us-east-1 --output json
221
+ ```
222
+
223
+ This creates/updates one secret named `my-app/dev` with a JSON payload like:
224
+
225
+ ```json
226
+ { "API_KEY": "abc123", "DATABASE_URL": "postgres://..." }
227
+ ```
228
+
229
+ 5. **Append/remove keys in an existing JSON secret:**
230
+
231
+ ```bash
232
+ env-secrets aws secret append -n my-app/dev --key JIRA_EMAIL_TOKEN -v blah -r us-east-1
233
+ env-secrets aws secret remove -n my-app/dev --key OLD_TOKEN -r us-east-1
234
+ ```
235
+
236
+ 6. **List secrets by prefix:**
237
+
238
+ ```bash
239
+ env-secrets aws secret list --prefix my-app/dev -r us-east-1 --output table
240
+ ```
241
+
242
+ Multi-region validation example:
243
+
244
+ ```bash
245
+ env-secrets aws secret list --prefix my-app/dev -r us-west-2 --output json
246
+ env-secrets aws secret list --prefix my-app/dev -r us-east-1 --output json
247
+ ```
248
+
249
+ 7. **Get metadata and version info (without printing secret value):**
250
+
251
+ ```bash
252
+ env-secrets aws secret get -n my-app/dev/api -r us-east-1 --output json
253
+ ```
254
+
255
+ 8. **Delete with explicit confirmation:**
256
+
257
+ ```bash
258
+ env-secrets aws secret delete -n my-app/dev/raw --recovery-days 7 --yes -r us-east-1
259
+ ```
260
+
261
+ ### Secret Management Safety Notes
262
+
263
+ - `delete` requires `--yes`.
264
+ - `create`/`update` accept `--value`, `--value-stdin`, or `--file` (use only one).
265
+ - `append` and `remove` require the secret value to be a JSON object.
266
+ - `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
+ - Use `--value-stdin` to avoid shell history leakage for sensitive values.
268
+ - Use either `--recovery-days` or `--force-delete-without-recovery` for delete operations.
269
+
143
270
  ## Examples
144
271
 
145
272
  ### Basic Usage
@@ -4,5 +4,6 @@ module.exports = {
4
4
  preset: 'ts-jest',
5
5
  testEnvironment: 'node',
6
6
  setupFilesAfterEnv: ['<rootDir>/__e2e__/setup.ts'],
7
+ testMatch: ['<rootDir>/__e2e__/**/*.test.ts'],
7
8
  testTimeout: 30000 // 30 seconds timeout for e2e tests
8
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "env-secrets",
3
- "version": "0.3.3",
3
+ "version": "0.5.0",
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)",
@@ -52,9 +52,9 @@
52
52
  "typescript": "^4.9.5"
53
53
  },
54
54
  "dependencies": {
55
- "@aws-sdk/client-secrets-manager": "^3.990.0",
56
- "@aws-sdk/client-sts": "^3.990.0",
57
- "@aws-sdk/credential-providers": "^3.990.0",
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",
58
58
  "commander": "^9.5.0",
59
59
  "debug": "^4.4.3"
60
60
  },
@@ -64,7 +64,7 @@
64
64
  "eslint --fix"
65
65
  ],
66
66
  "**/*.ts": [
67
- "tsc-files --noEmit --project src/tsconfig.json",
67
+ "bash -lc 'tsc --noEmit --project src/tsconfig.json'",
68
68
  "prettier --write",
69
69
  "eslint --fix"
70
70
  ],