@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 +19 -0
- package/commands/create/cmds/dekey.js +1 -1
- package/commands/create/cmds/diagnose.js +10 -0
- package/commands/create/cmds/key.js +21 -7
- package/commands/create/cmds/rekey.js +89 -24
- package/commands/create/libs/parallel-runner.js +19 -1
- package/commands/create/libs/vars.js +0 -4
- package/commands/create/services/aws/ses.js +9 -1
- package/globals.js +3 -0
- package/package.json +1 -1
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
|
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
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:
|
|
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
|
|
168
|
-
if (
|
|
169
|
-
const
|
|
170
|
-
users:
|
|
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
|
-
|
|
198
|
+
await aws.iam.removeIAMUser(`fw-automation-${user}`);
|
|
199
|
+
|
|
200
|
+
return aws.iam.createFWIAMUser(
|
|
176
201
|
`fw-automation-${user}`,
|
|
177
|
-
|
|
202
|
+
_.config.users.find((d) => d.username === user)
|
|
203
|
+
.permissions,
|
|
178
204
|
);
|
|
179
205
|
},
|
|
180
206
|
}));
|
|
181
207
|
|
|
182
|
-
const
|
|
183
|
-
concurrency:
|
|
184
|
-
verb: "Rotate
|
|
208
|
+
const runnerResults = await parallelRunner(runnerBatches, {
|
|
209
|
+
concurrency: 5,
|
|
210
|
+
verb: "Rotate runner credentials",
|
|
185
211
|
});
|
|
186
212
|
|
|
187
|
-
for (const [user, entries] of
|
|
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:
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
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 });
|
|
@@ -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
|
-
|
|
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"
|