badmfck-api-server 4.1.47 → 4.1.51

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.
@@ -7,5 +7,5 @@ interface IDeployerParams {
7
7
  includes?: string[];
8
8
  excludes?: string[];
9
9
  }
10
- export declare function Deploy(opt: IDeployerParams | string): Promise<string>;
10
+ export declare function Deploy(opt?: IDeployerParams | string | null): Promise<string>;
11
11
  export {};
@@ -10,21 +10,101 @@ const fs_1 = __importDefault(require("fs"));
10
10
  const __1 = require("../..");
11
11
  const crypto_1 = __importDefault(require("crypto"));
12
12
  const undici_1 = require("undici");
13
+ const promises_1 = require("readline/promises");
14
+ const process_1 = require("process");
15
+ function validateConfig(cfg) {
16
+ if (!cfg || typeof cfg !== "object")
17
+ return "not an object";
18
+ if (!cfg.name || !cfg.token || !cfg.host || !cfg.username || !cfg.password)
19
+ return "missing one of: name, token, host, username, password";
20
+ if (cfg.includes && !Array.isArray(cfg.includes))
21
+ return "includes must be an array";
22
+ if (cfg.excludes && !Array.isArray(cfg.excludes))
23
+ return "excludes must be an array";
24
+ return null;
25
+ }
26
+ async function selectConfig(configs) {
27
+ if (configs.length === 0) {
28
+ throw new Error("No valid deploy configs found");
29
+ }
30
+ if (configs.length === 1) {
31
+ console.log(`Using deploy config: ${configs[0].name}`);
32
+ return configs[0];
33
+ }
34
+ if (!process_1.stdin.isTTY) {
35
+ throw new Error(`Found ${configs.length} deploy configs (${configs.map(c => c.name).join(", ")}) but stdin is not a TTY — ` +
36
+ `cannot prompt for a choice. Pass a specific config explicitly, e.g. Deploy("deploy/<name>.json").`);
37
+ }
38
+ console.log("\nAvailable deploy configs:\n");
39
+ configs.forEach((config, index) => {
40
+ console.log(`[${index + 1}] ${config.name ?? "bad-config-file"}`);
41
+ });
42
+ const rl = (0, promises_1.createInterface)({
43
+ input: process_1.stdin,
44
+ output: process_1.stdout,
45
+ });
46
+ try {
47
+ while (true) {
48
+ const answer = await rl.question(`\nChoose config [1-${configs.length}]: `);
49
+ const selectedIndex = Number.parseInt(answer.trim(), 10) - 1;
50
+ if (Number.isInteger(selectedIndex) &&
51
+ selectedIndex >= 0 &&
52
+ selectedIndex < configs.length) {
53
+ return configs[selectedIndex];
54
+ }
55
+ console.error(`Invalid selection. Enter a number from 1 to ${configs.length}.`);
56
+ }
57
+ }
58
+ finally {
59
+ rl.close();
60
+ }
61
+ }
13
62
  async function Deploy(opt) {
63
+ if (!opt) {
64
+ const dir = path_1.default.resolve("deploy");
65
+ const single = path_1.default.resolve("deploy.json");
66
+ if (fs_1.default.existsSync(dir) && fs_1.default.statSync(dir).isDirectory()) {
67
+ const files = fs_1.default.readdirSync(dir).filter(f => f.endsWith(".json"));
68
+ if (files.length === 0)
69
+ throw new Error(`No .json configs found in ${dir}`);
70
+ const configs = [];
71
+ for (const f of files) {
72
+ const filePath = path_1.default.resolve(dir, f);
73
+ let parsed;
74
+ try {
75
+ parsed = JSON.parse(fs_1.default.readFileSync(filePath).toString("utf-8"));
76
+ }
77
+ catch (e) {
78
+ console.error(`Skipping ${f}: failed to parse JSON:`, e.message);
79
+ continue;
80
+ }
81
+ const reason = validateConfig(parsed);
82
+ if (reason) {
83
+ console.error(`Skipping ${f}: ${reason}`);
84
+ continue;
85
+ }
86
+ configs.push(parsed);
87
+ }
88
+ if (configs.length === 0)
89
+ throw new Error(`No valid deploy configs in ${dir} (all ${files.length} were skipped)`);
90
+ opt = await selectConfig(configs);
91
+ }
92
+ else if (fs_1.default.existsSync(single)) {
93
+ opt = single;
94
+ }
95
+ else {
96
+ throw new Error(`No deploy config found: neither ${dir}/ nor ${single} exists`);
97
+ }
98
+ }
14
99
  if (typeof opt === "string") {
15
100
  if (!fs_1.default.existsSync(opt)) {
16
101
  throw new Error(`File not found: ${opt}`);
17
102
  }
18
103
  opt = JSON.parse(fs_1.default.readFileSync(opt).toString("utf-8"));
19
- if (!opt.name || !opt.token || !opt.host || !opt.username || !opt.password) {
20
- throw new Error(`Invalid deploy config file: ${opt}`);
21
- }
22
- if (opt.includes && !Array.isArray(opt.includes)) {
23
- throw new Error(`Invalid deploy config file: includes must be an array`);
24
- }
25
- if (opt.excludes && !Array.isArray(opt.excludes)) {
26
- throw new Error(`Invalid deploy config file: excludes must be an array`);
27
- }
104
+ }
105
+ const invalidReason = validateConfig(opt);
106
+ if (invalidReason) {
107
+ throw new Error(`Invalid deploy config: ${invalidReason}`);
28
108
  }
29
109
  const archiveName = __1.UID.sha256(opt.name.replaceAll(".", "_")) + ".tar.gz";
30
110
  console.log("Changing Config to live");
@@ -173,35 +253,12 @@ async function Deploy(opt) {
173
253
  headersTimeout: 5 * 60 * 1000,
174
254
  bodyTimeout: 5 * 60 * 1000,
175
255
  });
176
- const abortController = new AbortController();
177
- const abortTimer = setTimeout(() => abortController.abort(), 5 * 60 * 1000);
178
- let uploadResponse;
179
- try {
180
- const res = await fetch(opt.host, {
181
- method: "POST",
182
- headers: { authorization: `Bearer ${authToken}` },
183
- body: formData,
184
- signal: abortController.signal,
185
- ...{ dispatcher: uploadAgent },
186
- });
187
- const rawText = await res.text();
188
- let parsed;
189
- try {
190
- parsed = rawText.length > 0 ? JSON.parse(rawText) : null;
191
- }
192
- catch {
193
- parsed = rawText;
194
- }
195
- uploadResponse = res.ok
196
- ? { ok: true, status: res.status, data: parsed }
197
- : { ok: false, status: res.status, error: parsed };
198
- }
199
- catch (err) {
200
- uploadResponse = { ok: false, error: err };
201
- }
202
- finally {
203
- clearTimeout(abortTimer);
204
- }
256
+ const uploadResponse = await __1.Http.post(opt.host, formData, {
257
+ headers: { authorization: `Bearer ${authToken}` },
258
+ dispatcher: uploadAgent,
259
+ timeoutMs: 5 * 60 * 1000,
260
+ retry: { enabled: false },
261
+ });
205
262
  const elapsedMs = Date.now() - startedAt;
206
263
  const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
207
264
  const c = (code, t) => useColor ? `\x1b[${code}m${t}\x1b[0m` : t;
@@ -281,18 +338,23 @@ async function Deploy(opt) {
281
338
  console.log(` ${dim("elapsed: ")} ${elapsedMs}ms`);
282
339
  console.log(` ${dim("time: ")} ${stamp}`);
283
340
  const info = body?.data?.data;
284
- if (info && typeof info === "object" && info.bluegreen === true) {
285
- const slotBadge = info.activeThisDeploy
286
- ? green(" active in production")
287
- : yellow("(inactive — deploy landed but nginx still points at " + info.active + "; run /pckg/switch to promote)");
288
- console.log(` ${dim("mode: ")} blue-green`);
289
- console.log(` ${dim("slot: ")} ${bold(String(info.slot))} ${slotBadge}`);
290
- if (info.active && info.active !== info.slot) {
291
- console.log(` ${dim("active: ")} ${info.active}`);
341
+ if (info && typeof info === "object" && "ok" in info) {
342
+ if (info.project && info.project !== opt.name) {
343
+ console.log(` ${dim("server project:")} ${info.project}`);
344
+ }
345
+ if (info.bluegreen === true) {
346
+ const slotBadge = info.activeThisDeploy
347
+ ? green("← active in production")
348
+ : yellow("(inactive — deploy landed but nginx still points at " + info.active + "; run /pckg/switch to promote)");
349
+ console.log(` ${dim("mode: ")} blue-green`);
350
+ console.log(` ${dim("slot: ")} ${bold(String(info.slot))} ${slotBadge}`);
351
+ if (info.active && info.active !== info.slot) {
352
+ console.log(` ${dim("active: ")} ${info.active}`);
353
+ }
354
+ }
355
+ else if (info.bluegreen === false) {
356
+ console.log(` ${dim("mode: ")} single-instance ${green("← active")}`);
292
357
  }
293
- }
294
- else if (info && typeof info === "object" && info.bluegreen === false) {
295
- console.log(` ${dim("mode: ")} single-instance`);
296
358
  }
297
359
  if (!emptyResponse) {
298
360
  const payload = JSON.stringify(body);
@@ -119,6 +119,7 @@ export declare class DeployerService extends BaseService {
119
119
  }>;
120
120
  tokensMatch(a: string | undefined, b: string | undefined): boolean;
121
121
  private _checkUserAuth;
122
+ private static _extractPort;
122
123
  private _renderNginx;
123
124
  runPM2(found: IConfig, projectState?: IProjectState | null): Promise<IError | null>;
124
125
  private static readonly SKIP_DIRS;
@@ -246,7 +246,7 @@ class DeployerService extends BaseService_1.BaseService {
246
246
  }
247
247
  if (found.nginx?.config_dir && (!projectState || projectStateWasCreated)) {
248
248
  const activeCfg = (found.config ?? {});
249
- const nginxErr = await this._renderNginx(found, data.name, activeCfg.PORT ?? "");
249
+ const nginxErr = await this._renderNginx(found, data.name, DeployerService._extractPort(activeCfg));
250
250
  if (nginxErr) {
251
251
  if (found.email && found.email.length > 0) {
252
252
  for (let email of found.email) {
@@ -366,7 +366,7 @@ class DeployerService extends BaseService_1.BaseService {
366
366
  if (found.nginx?.config_dir) {
367
367
  const slotCfg = target === "blue" ? found.bluegreen.blue_config : found.bluegreen.green_config;
368
368
  const merged = { ...(found.config ?? {}), ...(slotCfg ?? {}) };
369
- const nginxErr = await this._renderNginx(found, data.name, merged.PORT ?? "");
369
+ const nginxErr = await this._renderNginx(found, data.name, DeployerService._extractPort(merged));
370
370
  if (nginxErr) {
371
371
  (0, LogService_1.logError)("nginx render during switch failed (state already flipped): " + nginxErr.message);
372
372
  hookError = nginxErr.message;
@@ -441,11 +441,23 @@ class DeployerService extends BaseService_1.BaseService {
441
441
  }
442
442
  return matched;
443
443
  }
444
+ static _extractPort(config) {
445
+ if (!config || typeof config !== "object")
446
+ return "";
447
+ for (const key of Object.keys(config)) {
448
+ if (key.toLowerCase() === "port") {
449
+ const v = config[key];
450
+ if (v !== null && v !== undefined && String(v).length > 0)
451
+ return String(v);
452
+ }
453
+ }
454
+ return "";
455
+ }
444
456
  async _renderNginx(found, projectName, activePort) {
445
457
  if (!found.nginx?.config_dir)
446
458
  return null;
447
459
  if (!activePort) {
448
- return { code: 52, message: "Cannot render nginx: no PORT in merged config for project " + projectName, httpStatus: 500 };
460
+ return { code: 52, message: "Cannot render nginx: no port found in config for project " + projectName + " (expected a 'port' key in config or blue_config/green_config)", httpStatus: 500 };
449
461
  }
450
462
  const dir = found.nginx.config_dir;
451
463
  try {
@@ -43,6 +43,32 @@ function normalizeHeaders(headers) {
43
43
  function isRetryableStatus(status, retryOnStatuses) {
44
44
  return retryOnStatuses.includes(status);
45
45
  }
46
+ function isFormDataLike(value) {
47
+ return (value !== null &&
48
+ typeof value === "object" &&
49
+ Object.prototype.toString.call(value) === "[object FormData]" &&
50
+ typeof value.entries === "function");
51
+ }
52
+ function normalizeFormData(source) {
53
+ if (source instanceof undici_1.FormData)
54
+ return source;
55
+ const target = new undici_1.FormData();
56
+ for (const [name, value] of source.entries()) {
57
+ if (typeof value === "string") {
58
+ target.append(name, value);
59
+ }
60
+ else {
61
+ target.append(name, value, value.name);
62
+ }
63
+ }
64
+ return target;
65
+ }
66
+ function removeContentTypeHeader(headers) {
67
+ for (const key of Object.keys(headers)) {
68
+ if (key.toLowerCase() === "content-type")
69
+ delete headers[key];
70
+ }
71
+ }
46
72
  function isLikelyNetworkError(err) {
47
73
  if (!err || typeof err !== "object")
48
74
  return false;
@@ -423,11 +449,12 @@ class Http {
423
449
  let body = undefined;
424
450
  const hasBody = opts.body !== undefined && opts.body !== null;
425
451
  const allowBody = opts.allowBody ?? (opts.method !== "GET" && opts.method !== "DELETE");
452
+ let isMultipart = false;
426
453
  if (hasBody && allowBody) {
427
- if (typeof FormData !== "undefined" && opts.body instanceof FormData) {
428
- body = opts.body;
429
- delete headers["content-type"];
430
- delete headers["Content-Type"];
454
+ if (isFormDataLike(opts.body)) {
455
+ isMultipart = true;
456
+ body = normalizeFormData(opts.body);
457
+ removeContentTypeHeader(headers);
431
458
  }
432
459
  else {
433
460
  const ct = (headers["content-type"] || headers["Content-Type"] || "").toLowerCase();
@@ -442,6 +469,9 @@ class Http {
442
469
  }
443
470
  }
444
471
  }
472
+ if (isMultipart && retryCfg.maxAttempts !== 1) {
473
+ retryCfg.maxAttempts = 1;
474
+ }
445
475
  const responseType = opts.responseType ?? "json";
446
476
  const throwOnJsonParseError = opts.throwOnJsonParseError ?? false;
447
477
  let attemptsMade = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badmfck-api-server",
3
- "version": "4.1.47",
3
+ "version": "4.1.51",
4
4
  "description": "Simple API http server based on express",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",