@fishawack/lab-env 5.7.0-beta.5 → 5.7.0-beta.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## Changelog
2
2
 
3
+ ### 5.7.0-beta.7 (2026-07-03)
4
+
5
+ #### Bug Fixes
6
+
7
+ * added more places where rekey can bypass without project ([44e8ca4](https://bitbucket.org/fishawackdigital/lab-env/commits/44e8ca4ec2a1199d96e857338516628d51cb7e1d))
8
+ * remove boilerplate-vue as option now its deprecated ([e8c097a](https://bitbucket.org/fishawackdigital/lab-env/commits/e8c097af21260c127defb90bb85c0eed224f35c2))
9
+ * rotate email creds before operator iam permissions ([a4f510e](https://bitbucket.org/fishawackdigital/lab-env/commits/a4f510e916912dd199d03e4fde984c4abbb8d931))
10
+
11
+ ### 5.7.0-beta.6 (2026-07-03)
12
+
13
+ #### Features
14
+
15
+ * **devops:** misc.json setup, SES rotation recovery, runner ordering ([3c6ec70](https://bitbucket.org/fishawackdigital/lab-env/commits/3c6ec70df5e54ad4683a9a5b4e8e3accb681121d))
16
+
17
+ #### Bug Fixes
18
+
19
+ * add timeout for aws key calls themselves ([389444c](https://bitbucket.org/fishawackdigital/lab-env/commits/389444cf7c0c61273b8dc7edf2d0d6695a1047a7))
20
+ * concurrency set back to 5 for more stable key commands ([a5e7382](https://bitbucket.org/fishawackdigital/lab-env/commits/a5e7382b9731ad2a5a60110a54e27ebe4af45391))
21
+
3
22
  ### 5.7.0-beta.5 (2026-07-03)
4
23
 
5
24
  #### Bug Fixes
@@ -75,7 +75,7 @@ module.exports = [
75
75
  }));
76
76
 
77
77
  await parallelRunner(batches, {
78
- concurrency: 10,
78
+ concurrency: 5,
79
79
  verb: "Remove credentials",
80
80
  });
81
81
  },
@@ -142,6 +142,16 @@ module.exports = [
142
142
  }
143
143
  }
144
144
 
145
+ if (preset === "devops") {
146
+ while (!(await test.targets())) {
147
+ await guide.targets(argv);
148
+ }
149
+
150
+ while (!(await test.misc())) {
151
+ await guide.misc(argv);
152
+ }
153
+ }
154
+
145
155
  // Update diagnosis version in ~/.lab-env config file
146
156
  let spinner = new Spinner(
147
157
  `Writing options to ${path.join(os.homedir(), "", ".lab-env")}`,
@@ -95,18 +95,32 @@ module.exports = [
95
95
 
96
96
  if (answer.check) {
97
97
  try {
98
- const creds = await aws.ses.ensureOperatorSESCredentials(
98
+ let creds = await aws.ses.ensureOperatorSESCredentials(
99
99
  operator.username,
100
100
  );
101
101
 
102
+ if (!creds) {
103
+ answer = await inquirer.prompt([
104
+ {
105
+ type: "confirm",
106
+ name: "check",
107
+ message:
108
+ "SES IAM user exists but secret is not stored locally. Rotate credentials to recapture?",
109
+ default: "Y",
110
+ },
111
+ ]);
112
+
113
+ if (answer.check) {
114
+ creds = await aws.ses.rotateOperatorSESCredentials(
115
+ operator.username,
116
+ );
117
+ }
118
+ }
119
+
102
120
  if (creds) {
103
121
  aws.ses.updateMiscNodemailer(creds);
104
122
  console.log(
105
- "\n ✓ SES email credentials created and saved to misc.json\n",
106
- );
107
- } else {
108
- console.log(
109
- "\n ⚠ SES IAM user exists but keys were already created. Rotate with `fw rekey` to generate new credentials.\n",
123
+ "\n ✓ SES email credentials saved to misc.json\n",
110
124
  );
111
125
  }
112
126
  } catch (err) {
@@ -133,7 +147,7 @@ module.exports = [
133
147
  }));
134
148
 
135
149
  const resultsMap = await parallelRunner(batches, {
136
- concurrency: 10,
150
+ concurrency: 5,
137
151
  verb: "Create credentials",
138
152
  });
139
153
 
@@ -95,18 +95,32 @@ module.exports = [
95
95
 
96
96
  if (answer.check) {
97
97
  try {
98
- const creds = await aws.ses.ensureOperatorSESCredentials(
98
+ let creds = await aws.ses.ensureOperatorSESCredentials(
99
99
  operator.username,
100
100
  );
101
101
 
102
+ if (!creds) {
103
+ answer = await inquirer.prompt([
104
+ {
105
+ type: "confirm",
106
+ name: "check",
107
+ message:
108
+ "SES IAM user exists but secret is not stored locally. Rotate credentials to recapture?",
109
+ default: "Y",
110
+ },
111
+ ]);
112
+
113
+ if (answer.check) {
114
+ creds = await aws.ses.rotateOperatorSESCredentials(
115
+ operator.username,
116
+ );
117
+ }
118
+ }
119
+
102
120
  if (creds) {
103
121
  aws.ses.updateMiscNodemailer(creds);
104
122
  console.log(
105
- "\n \u2713 SES email credentials created and saved to misc.json\n",
106
- );
107
- } else {
108
- console.log(
109
- "\n \u26a0 SES IAM user exists but keys were already created. Rotate with `fw rekey` to generate new credentials.\n",
123
+ "\n \u2713 SES email credentials saved to misc.json\n",
110
124
  );
111
125
  }
112
126
  } catch (err) {
@@ -117,10 +131,19 @@ module.exports = [
117
131
  }
118
132
  }
119
133
 
120
- // Separate non-operator users and operator
121
- // Operator MUST be processed last per account rotating the
122
- // operator's key invalidates the credentials used for all API calls
123
- let nonOperatorUsers = users.filter((u) => u !== operator.username);
134
+ // Separate users into: regular, runners, operator
135
+ // Processing order: regular users runners operator (last)
136
+ let runners = users.filter((u) => {
137
+ let config = _.config.users.find((d) => d.username === u);
138
+ return (
139
+ u !== operator.username &&
140
+ config &&
141
+ (config.runner || u === "aws-runner")
142
+ );
143
+ });
144
+ let nonOperatorUsers = users.filter(
145
+ (u) => u !== operator.username && !runners.includes(u),
146
+ );
124
147
  let includeOperator = users.includes(operator.username);
125
148
 
126
149
  let credentials = {};
@@ -144,7 +167,7 @@ module.exports = [
144
167
  }));
145
168
 
146
169
  const resultsMap = await parallelRunner(batches, {
147
- concurrency: 10,
170
+ concurrency: 5,
148
171
  verb: "Rotate credentials",
149
172
  });
150
173
 
@@ -164,39 +187,49 @@ module.exports = [
164
187
  }
165
188
  }
166
189
 
167
- // Process operator sequentially must be last per account
168
- if (includeOperator) {
169
- const operatorBatches = clients.map((client) => ({
170
- users: [operator.username],
190
+ // Process runner users as a group (after regular, before operator)
191
+ if (runners.length) {
192
+ const runnerBatches = clients.map((client) => ({
193
+ users: runners,
171
194
  client,
172
195
  fn: async (user) => {
173
196
  setAWSClientDefaults(client);
174
197
 
175
- return aws.iam.rotateFWIAMUser(
198
+ await aws.iam.removeIAMUser(`fw-automation-${user}`);
199
+
200
+ return aws.iam.createFWIAMUser(
176
201
  `fw-automation-${user}`,
177
- operator.permissions,
202
+ _.config.users.find((d) => d.username === user)
203
+ .permissions,
178
204
  );
179
205
  },
180
206
  }));
181
207
 
182
- const operatorResults = await parallelRunner(operatorBatches, {
183
- concurrency: 1,
184
- verb: "Rotate operator credentials",
208
+ const runnerResults = await parallelRunner(runnerBatches, {
209
+ concurrency: 5,
210
+ verb: "Rotate runner credentials",
185
211
  });
186
212
 
187
- for (const [user, entries] of operatorResults) {
213
+ for (const [user, entries] of runnerResults) {
188
214
  credentials[user] = {};
189
215
 
190
216
  for (const { client, res } of entries) {
191
217
  credentials[user][client] = {
192
- key: res.AccessKey.AccessKeyId,
193
- secret: res.AccessKey.SecretAccessKey,
218
+ key:
219
+ (res.AccessKey && res.AccessKey.AccessKeyId) ||
220
+ res.AccessKeyMetadata[0].AccessKeyId,
221
+ secret:
222
+ (res.AccessKey && res.AccessKey.SecretAccessKey) ||
223
+ "** secret **",
194
224
  };
195
225
  }
196
226
  }
197
227
  }
198
228
 
199
- // Rotate SES email credentials (after operator keys so fishawack creds are still valid)
229
+ // Rotate SES email credentials BEFORE rotating the operator's AWS key.
230
+ // The SES client uses fromIni({ profile: 'fishawack' }) which reads the
231
+ // operator's key from ~/.aws/credentials — once that key is rotated below
232
+ // the profile becomes stale and the SES call would fail.
200
233
  if (misc.nodemailer?.AWSSES?.username) {
201
234
  try {
202
235
  const creds = await aws.ses.rotateOperatorSESCredentials(
@@ -213,6 +246,38 @@ module.exports = [
213
246
  }
214
247
  }
215
248
 
249
+ // Process operator sequentially — must be last per account
250
+ if (includeOperator) {
251
+ const operatorBatches = clients.map((client) => ({
252
+ users: [operator.username],
253
+ client,
254
+ fn: async (user) => {
255
+ setAWSClientDefaults(client);
256
+
257
+ return aws.iam.rotateFWIAMUser(
258
+ `fw-automation-${user}`,
259
+ operator.permissions,
260
+ );
261
+ },
262
+ }));
263
+
264
+ const operatorResults = await parallelRunner(operatorBatches, {
265
+ concurrency: 1,
266
+ verb: "Rotate operator credentials",
267
+ });
268
+
269
+ for (const [user, entries] of operatorResults) {
270
+ credentials[user] = {};
271
+
272
+ for (const { client, res } of entries) {
273
+ credentials[user][client] = {
274
+ key: res.AccessKey.AccessKeyId,
275
+ secret: res.AccessKey.SecretAccessKey,
276
+ };
277
+ }
278
+ }
279
+ }
280
+
216
281
  await outputCredentials(credentials, sendEmail);
217
282
  },
218
283
  ];
@@ -6,6 +6,7 @@ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧",
6
6
  const MAX_RETRIES = 15;
7
7
  const INITIAL_DELAY = 2000;
8
8
  const MAX_DELAY = 60000;
9
+ const TASK_TIMEOUT = 120000; // 2 min hard timeout per attempt — prevents hung TCP connections
9
10
 
10
11
  const RETRYABLE_ERRORS = [
11
12
  "Throttling",
@@ -180,7 +181,24 @@ module.exports = async function parallelRunner(
180
181
  state.taskStart = Date.now();
181
182
 
182
183
  try {
183
- const res = await batch.fn(user);
184
+ let timeoutHandle;
185
+ const res = await Promise.race([
186
+ batch.fn(user),
187
+ new Promise((_, reject) => {
188
+ timeoutHandle = setTimeout(
189
+ () =>
190
+ reject(
191
+ Object.assign(
192
+ new Error(
193
+ "Request timed out after 2 minutes — will retry",
194
+ ),
195
+ { name: "RequestTimeout" },
196
+ ),
197
+ ),
198
+ TASK_TIMEOUT,
199
+ );
200
+ }),
201
+ ]).finally(() => clearTimeout(timeoutHandle));
184
202
  results
185
203
  .get(user)
186
204
  .push({ client: batch.client, res });
@@ -114,10 +114,6 @@ module.exports.templates = [
114
114
  name: "boilerplate",
115
115
  type: "boilerplate",
116
116
  },
117
- {
118
- name: "boilerplate-vue",
119
- type: "boilerplate",
120
- },
121
117
  {
122
118
  name: "boilerplate-wordpress",
123
119
  type: "boilerplate",
@@ -176,7 +176,15 @@ const MISC_PATH = `${os.homedir()}/targets/misc.json`;
176
176
  module.exports.updateMiscNodemailer = (creds) => {
177
177
  const { misc } = require("../../libs/vars");
178
178
 
179
- const file = JSON.parse(readFileSync(MISC_PATH, { encoding: "utf8" }));
179
+ let file = {};
180
+
181
+ try {
182
+ file = JSON.parse(readFileSync(MISC_PATH, { encoding: "utf8" }));
183
+ } catch {
184
+ const { mkdirSync } = require("fs");
185
+ mkdirSync(require("path").dirname(MISC_PATH), { recursive: true });
186
+ file = { bitbucket: {}, gitlab: {} };
187
+ }
180
188
 
181
189
  file.nodemailer = {
182
190
  driver: "AWSSES",
package/globals.js CHANGED
@@ -233,6 +233,7 @@ if (composer && composer.require && composer.require["laravel/framework"]) {
233
233
  args[0] !== "new" &&
234
234
  args[0] !== "key" &&
235
235
  args[0] !== "dekey" &&
236
+ args[0] !== "rekey" &&
236
237
  args[0] !== "lint" &&
237
238
  args[0] !== "workspace" &&
238
239
  args[0] !== "prune"
@@ -355,6 +356,7 @@ if (process.env.FW_NEXT || forced) {
355
356
  args[0] !== "new" &&
356
357
  args[0] !== "key" &&
357
358
  args[0] !== "dekey" &&
359
+ args[0] !== "rekey" &&
358
360
  args[0] !== "lint" &&
359
361
  args[0] !== "workspace" &&
360
362
  args[0] !== "prune"
@@ -715,6 +717,7 @@ if (
715
717
  args[0] !== "--help" &&
716
718
  args[0] !== "key" &&
717
719
  args[0] !== "dekey" &&
720
+ args[0] !== "rekey" &&
718
721
  args[0] !== "lint" &&
719
722
  args[0] !== "workspace" &&
720
723
  args[0] !== "prune"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-env",
3
- "version": "5.7.0-beta.5",
3
+ "version": "5.7.0-beta.7",
4
4
  "description": "Docker manager for FW",
5
5
  "main": "cli.js",
6
6
  "scripts": {