cloudron 5.13.0 → 5.14.1

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/bin/cloudron CHANGED
@@ -10,7 +10,7 @@ const actions = require('../src/actions.js'),
10
10
  Command = require('commander').Command,
11
11
  safe = require('safetydance'),
12
12
  semver = require('semver'),
13
- superagent = require('superagent'),
13
+ superagent = require('../src/superagent.js'),
14
14
  util = require('util');
15
15
 
16
16
  const version = require('../package.json').version;
@@ -293,7 +293,7 @@ program.command('update')
293
293
  if (Date.now() - (config.get('lastCliUpdateCheck') || 0) > 24*60*60*1000) {
294
294
  // check if cli tool is up-to-date
295
295
  const [error, response] = await safe(superagent.get('https://registry.npmjs.org/cloudron').retry(0).ok(() => true));
296
- if (!error && response.statusCode === 200 && response.body['dist-tags'].latest !== version) {
296
+ if (!error && response.status === 200 && response.body['dist-tags'].latest !== version) {
297
297
  const updateCommand = 'npm install -g cloudron@' + response.body['dist-tags'].latest;
298
298
  process.stderr.write(util.format('A new version of Cloudron CLI is available. Please update with: %s\n', updateCommand));
299
299
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudron",
3
- "version": "5.13.0",
3
+ "version": "5.14.1",
4
4
  "license": "MIT",
5
5
  "description": "Cloudron Commandline Tool",
6
6
  "main": "main.js",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "author": "Cloudron Developers <support@cloudron.io>",
19
19
  "dependencies": {
20
- "cloudron-manifestformat": "^5.26.2",
20
+ "cloudron-manifestformat": "^5.27.0",
21
21
  "commander": "^13.1.0",
22
22
  "debug": "^4.4.0",
23
23
  "easy-table": "^1.2.0",
@@ -26,15 +26,14 @@
26
26
  "micromatch": "^4.0.8",
27
27
  "open": "^10.1.0",
28
28
  "safetydance": "^2.5.0",
29
- "superagent": "^10.1.1",
30
29
  "tar-fs": "^3.0.8"
31
30
  },
32
31
  "engines": {
33
32
  "node": ">= 18.x.x"
34
33
  },
35
34
  "devDependencies": {
36
- "@eslint/js": "^9.20.0",
37
- "eslint": "^9.20.1",
35
+ "@eslint/js": "^9.22.0",
36
+ "eslint": "^9.22.0",
38
37
  "expect.js": "^0.3.1",
39
38
  "mocha": "^11.1.0"
40
39
  }
package/src/actions.js CHANGED
@@ -15,7 +15,7 @@ const assert = require('assert'),
15
15
  safe = require('safetydance'),
16
16
  spawn = require('child_process').spawn,
17
17
  semver = require('semver'),
18
- superagent = require('superagent'),
18
+ superagent = require('./superagent.js'),
19
19
  Table = require('easy-table'),
20
20
  tar = require('tar-fs'),
21
21
  timers = require('timers/promises'),
@@ -79,7 +79,7 @@ function createRequest(method, apiPath, options) {
79
79
  let url = `https://${adminFqdn}${apiPath}`;
80
80
  if (url.includes('?')) url += '&'; else url += '?';
81
81
  url += `access_token=${token}`;
82
- const request = superagent(method, url);
82
+ const request = superagent.request(method, url);
83
83
  if (!rejectUnauthorized) request.disableTLSCerts();
84
84
  request.retry(3);
85
85
  request.ok(() => true);
@@ -88,9 +88,9 @@ function createRequest(method, apiPath, options) {
88
88
 
89
89
  // error for the request module
90
90
  function requestError(response) {
91
- if (response.statusCode === 401) return 'Invalid token. Use cloudron login again.';
91
+ if (response.status === 401) return 'Invalid token. Use cloudron login again.';
92
92
 
93
- return `${response.statusCode} message: ${response.body.message || JSON.stringify(response.body)}`; // body is sometimes just a string like in 401
93
+ return `${response.status} message: ${response.body.message || JSON.stringify(response.body)}`; // body is sometimes just a string like in 401
94
94
  }
95
95
 
96
96
  async function selectDomain(location, options) {
@@ -100,7 +100,7 @@ async function selectDomain(location, options) {
100
100
  const { adminFqdn } = requestOptions(options);
101
101
 
102
102
  const response = await createRequest('GET', '/api/v1/domains', options);
103
- if (response.statusCode !== 200) throw new Error(`Failed to list domains: ${requestError(response)}`);
103
+ if (response.status !== 200) throw new Error(`Failed to list domains: ${requestError(response)}`);
104
104
 
105
105
  const domains = response.body.domains;
106
106
 
@@ -134,7 +134,7 @@ async function stopActiveTask(app, options) {
134
134
  console.log(`Stopping app's current active task ${app.taskId}`);
135
135
 
136
136
  const response = await createRequest('POST', `/api/v1/tasks/${app.taskId}/stop`, options);
137
- if (response.statusCode !== 204) throw `Failed to stop active task: ${requestError(response)}`;
137
+ if (response.status !== 204) throw `Failed to stop active task: ${requestError(response)}`;
138
138
  }
139
139
 
140
140
  async function selectAppWithRepository(repository, options) {
@@ -142,7 +142,7 @@ async function selectAppWithRepository(repository, options) {
142
142
  assert.strictEqual(typeof options, 'object');
143
143
 
144
144
  const response = await createRequest('GET', '/api/v1/apps', options);
145
- if (response.statusCode !== 200) throw new Error(`Failed to install app: ${requestError(response)}`);
145
+ if (response.status !== 200) throw new Error(`Failed to install app: ${requestError(response)}`);
146
146
 
147
147
  const matchingApps = response.body.apps.filter(function (app) {
148
148
  return !app.appStoreId && app.manifest.dockerImage.startsWith(repository); // never select apps from the store
@@ -190,12 +190,12 @@ async function getApp(options) {
190
190
  return result;
191
191
  } else if (app.match(/.{8}-.{4}-.{4}-.{4}-.{8}/)) { // it is an id
192
192
  const response = await createRequest('GET', `/api/v1/apps/${app}`, options);
193
- if (response.statusCode !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
193
+ if (response.status !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
194
194
 
195
195
  return response.body;
196
196
  } else { // it is a location
197
197
  const response = await createRequest('GET', '/api/v1/apps', options);
198
- if (response.statusCode !== 200) throw new Error(`Failed to get apps: ${requestError(response)}`);
198
+ if (response.status !== 200) throw new Error(`Failed to get apps: ${requestError(response)}`);
199
199
 
200
200
  const match = response.body.apps.filter(function (m) { return m.subdomain === app || m.location === app || m.fqdn === app; });
201
201
  if (match.length == 0) throw new Error(`App at location ${app} not found`);
@@ -215,7 +215,7 @@ async function waitForHealthy(appId, options) {
215
215
  while (true) {
216
216
  await timers.setTimeout(1000);
217
217
  const response = await createRequest('GET', `/api/v1/apps/${appId}`, options);
218
- if (response.statusCode !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
218
+ if (response.status !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
219
219
 
220
220
  // do not check installation state here. it can be pending_backup etc (this is a bug in box code)
221
221
  if (response.body.health === 'healthy') return;
@@ -238,7 +238,7 @@ async function waitForTask(taskId, options) {
238
238
 
239
239
  while (true) {
240
240
  const response = await createRequest('GET', `/api/v1/tasks/${taskId}`, options);
241
- if (response.statusCode !== 200) throw new Error(`Failed to get task: ${requestError(response)}`);
241
+ if (response.status !== 200) throw new Error(`Failed to get task: ${requestError(response)}`);
242
242
 
243
243
  if (!response.body.active) return response.body;
244
244
 
@@ -272,7 +272,7 @@ async function waitForFinishInstallation(appId, taskId, options) {
272
272
  await waitForTask(taskId, options);
273
273
 
274
274
  const response = await createRequest('GET', `/api/v1/apps/${appId}`, options);
275
- if (response.statusCode !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
275
+ if (response.status !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
276
276
 
277
277
  if (response.body.installationState !== 'installed') throw new Error(`Installation failed: ${response.body.error ? response.body.error.message : ''}`);
278
278
 
@@ -289,7 +289,7 @@ async function waitForFinishBackup(appId, taskId, options) {
289
289
  if (result.error) throw new Error(`Backup failed: ${result.error.message}`);
290
290
 
291
291
  const response = await createRequest('GET', `/api/v1/apps/${appId}`, options);
292
- if (response.statusCode !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
292
+ if (response.status !== 200) throw new Error(`Failed to get app: ${requestError(response)}`);
293
293
  }
294
294
 
295
295
  async function stopApp(app, options) {
@@ -297,7 +297,7 @@ async function stopApp(app, options) {
297
297
  assert.strictEqual(typeof options, 'object');
298
298
 
299
299
  const response = await createRequest('POST', `/api/v1/apps/${app.id}/stop`, options);
300
- if (response.statusCode !== 202) throw `Failed to stop app: ${requestError(response)}`;
300
+ if (response.status !== 202) throw `Failed to stop app: ${requestError(response)}`;
301
301
 
302
302
  await waitForTask(response.body.taskId, options);
303
303
  }
@@ -307,7 +307,7 @@ async function startApp(app, options) {
307
307
  assert.strictEqual(typeof options, 'object');
308
308
 
309
309
  const response = await createRequest('POST', `/api/v1/apps/${app.id}/start`, options);
310
- if (response.statusCode !== 202) throw `Failed to start app: ${requestError(response)}`;
310
+ if (response.status !== 202) throw `Failed to start app: ${requestError(response)}`;
311
311
 
312
312
  await waitForTask(response.body.taskId, options);
313
313
  }
@@ -317,7 +317,7 @@ async function restartApp(app, options) {
317
317
  assert.strictEqual(typeof options, 'object');
318
318
 
319
319
  const response = await createRequest('POST', `/api/v1/apps/${app.id}/restart`, options);
320
- if (response.statusCode !== 202) throw `Failed to restart app: ${requestError(response)}`;
320
+ if (response.status !== 202) throw `Failed to restart app: ${requestError(response)}`;
321
321
 
322
322
  await waitForTask(response.body.taskId, options);
323
323
  }
@@ -351,7 +351,7 @@ async function authenticate(adminFqdn, username, password, options) {
351
351
  }
352
352
  }
353
353
 
354
- if (response.statusCode !== 200) throw new Error(`Login failed: Status code: ${requestError(response)}`);
354
+ if (response.status !== 200) throw new Error(`Login failed: Status code: ${requestError(response)}`);
355
355
 
356
356
  return response.body.accessToken;
357
357
  }
@@ -425,7 +425,7 @@ async function list(localOptions, cmd) {
425
425
  const options = cmd.optsWithGlobals();
426
426
  const [error, response] = await safe(createRequest('GET', '/api/v1/apps', options));
427
427
  if (error) return exit(error);
428
- if (response.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
428
+ if (response.status !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
429
429
 
430
430
  let apps = response.body.apps;
431
431
 
@@ -447,7 +447,7 @@ async function list(localOptions, cmd) {
447
447
 
448
448
  const response = result.value;
449
449
 
450
- if (response.statusCode !== 200) return exit(`Failed to list app: ${requestError(response)}`);
450
+ if (response.status !== 200) return exit(`Failed to list app: ${requestError(response)}`);
451
451
  response.body.location = response.body.location || response.body.subdomain; // LEGACY support
452
452
 
453
453
  const detailedApp = response.body;
@@ -513,7 +513,7 @@ async function downloadManifest(appstoreId) {
513
513
 
514
514
  const [error, response] = await safe(superagent.get(url).ok(() => true));
515
515
  if (error) throw new Error(`Failed to list apps from appstore: ${error.message}`);
516
- if (response.statusCode !== 200) throw new Error(`Failed to get app info from store: ${requestError(response)}`);
516
+ if (response.status !== 200) throw new Error(`Failed to get app info from store: ${requestError(response)}`);
517
517
 
518
518
  return { manifest: response.body.manifest, manifestFilePath: null /* manifest file path */ };
519
519
  }
@@ -647,7 +647,7 @@ async function install(localOptions, cmd) {
647
647
 
648
648
  const request = createRequest('POST', '/api/v1/apps/install', options);
649
649
  const response = await request.send(data);
650
- if (response.statusCode !== 202) return exit(`Failed to install app: ${requestError(response)}`);
650
+ if (response.status !== 202) return exit(`Failed to install app: ${requestError(response)}`);
651
651
 
652
652
  const appId = response.body.id;
653
653
 
@@ -744,7 +744,7 @@ async function setLocation(localOptions, cmd) {
744
744
 
745
745
  const request = createRequest('POST', `/api/v1/apps/${app.id}/configure/location`, options);
746
746
  const response = await request.send(data);
747
- if (response.statusCode !== 202) return exit(`Failed to configure app: ${requestError(response)}`);
747
+ if (response.status !== 202) return exit(`Failed to configure app: ${requestError(response)}`);
748
748
 
749
749
  await waitForTask(response.body.taskId, options);
750
750
  await waitForHealthy(app.id, options);
@@ -800,7 +800,7 @@ async function update(localOptions, cmd) {
800
800
 
801
801
  const request = createRequest('POST', apiPath, options);
802
802
  const response = await request.send(data);
803
- if (response.statusCode !== 202) return exit(`Failed to update app: ${requestError(response)}`);
803
+ if (response.status !== 202) return exit(`Failed to update app: ${requestError(response)}`);
804
804
 
805
805
  process.stdout.write('\n => ' + 'Waiting for app to be updated ');
806
806
 
@@ -829,7 +829,7 @@ async function debug(args, localOptions, cmd) {
829
829
 
830
830
  const request = createRequest('POST', `/api/v1/apps/${app.id}/configure/debug_mode`, options);
831
831
  const response = await request.send(data);
832
- if (response.statusCode !== 202) return exit(`Failed to set debug mode: ${requestError(response)}`);
832
+ if (response.status !== 202) return exit(`Failed to set debug mode: ${requestError(response)}`);
833
833
 
834
834
  await waitForTask(response.body.taskId, options);
835
835
 
@@ -843,7 +843,7 @@ async function debug(args, localOptions, cmd) {
843
843
 
844
844
  const request2 = createRequest('POST', `/api/v1/apps/${app.id}/configure/memory_limit`, options);
845
845
  const response2 = await request2.send({ memoryLimit });
846
- if (response2.statusCode !== 202) return exit(`Failed to set memory limit: ${requestError(response2)}`);
846
+ if (response2.status !== 202) return exit(`Failed to set memory limit: ${requestError(response2)}`);
847
847
 
848
848
  await waitForTask(response2.body.taskId, options);
849
849
  console.log('\n\nDone');
@@ -863,7 +863,7 @@ async function repair(localOptions, cmd) {
863
863
 
864
864
  const request = createRequest('POST', `/api/v1/apps/${app.id}/repair`, options);
865
865
  const response = await request.send(data);
866
- if (response.statusCode !== 202) return exit(`Failed to set repair mode: ${requestError(response)}`);
866
+ if (response.status !== 202) return exit(`Failed to set repair mode: ${requestError(response)}`);
867
867
 
868
868
  process.stdout.write('\n => ' + 'Waiting for app to be repaired ');
869
869
 
@@ -896,13 +896,13 @@ async function uninstall(localOptions, cmd) {
896
896
  await stopActiveTask(app, options);
897
897
 
898
898
  const response = await createRequest('POST', `/api/v1/apps/${app.id}/uninstall`, options);
899
- if (response.statusCode !== 202) return exit(`Failed to uninstall app: ${requestError(response)}`);
899
+ if (response.status !== 202) return exit(`Failed to uninstall app: ${requestError(response)}`);
900
900
 
901
901
  process.stdout.write('\n => ' + 'Waiting for app to be uninstalled ');
902
902
 
903
903
  await waitForTask(response.body.taskId, options);
904
904
  const response2 = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
905
- if (response2.statusCode === 404) {
905
+ if (response2.status === 404) {
906
906
  console.log('\n\nApp %s successfully uninstalled.', app.fqdn);
907
907
  } else if (response2.body.installationState === 'error') {
908
908
  console.log('\n\nApp uninstallation failed.\n');
@@ -968,7 +968,7 @@ async function logs(localOptions, cmd) {
968
968
 
969
969
  const req = superagent.get(url, { rejectUnauthorized });
970
970
  req.on('response', function (response) {
971
- if (response.statusCode !== 200) return exit(`Failed to get logs: ${requestError(response)}`);
971
+ if (response.status !== 200) return exit(`Failed to get logs: ${requestError(response)}`);
972
972
  });
973
973
  req.on('error', (error) => exit(`Pipe error: ${error.message}`));
974
974
 
@@ -1003,13 +1003,13 @@ async function inspect(localOptions, cmd) {
1003
1003
  try {
1004
1004
  const options = cmd.optsWithGlobals();
1005
1005
  const response = await createRequest('GET', '/api/v1/apps', options);
1006
- if (response.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
1006
+ if (response.status !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
1007
1007
 
1008
1008
  const apps = [];
1009
1009
 
1010
1010
  for (const app of response.body.apps) {
1011
1011
  const response2 = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1012
- if (response2.statusCode !== 200) return exit(`Failed to list app: ${requestError(response2)}`);
1012
+ if (response2.status !== 200) return exit(`Failed to list app: ${requestError(response2)}`);
1013
1013
  response2.body.location = response2.body.location || response2.body.subdomain; // LEGACY support
1014
1014
  apps.push(response2.body);
1015
1015
  }
@@ -1070,7 +1070,7 @@ async function backupCreate(localOptions, cmd) {
1070
1070
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1071
1071
 
1072
1072
  const response = await createRequest('POST', `/api/v1/apps/${app.id}/backup`, options);
1073
- if (response.statusCode !== 202) return exit(`Failed to start backup: ${requestError(response)}`);
1073
+ if (response.status !== 202) return exit(`Failed to start backup: ${requestError(response)}`);
1074
1074
 
1075
1075
  // FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
1076
1076
  await waitForFinishBackup(app.id, response.body.taskId, options);
@@ -1087,7 +1087,7 @@ async function backupList(localOptions, cmd) {
1087
1087
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1088
1088
 
1089
1089
  const response = await createRequest('GET', `/api/v1/apps/${app.id}/backups`, options);
1090
- if (response.statusCode !== 200) return exit(`Failed to list backups: ${requestError(response)}`);
1090
+ if (response.status !== 200) return exit(`Failed to list backups: ${requestError(response)}`);
1091
1091
 
1092
1092
  if (options.raw) return console.log(JSON.stringify(response.body.backups, null, 4));
1093
1093
 
@@ -1117,7 +1117,7 @@ async function backupList(localOptions, cmd) {
1117
1117
 
1118
1118
  async function getLastBackup(app, options) {
1119
1119
  const response = await createRequest('GET', `/api/v1/apps/${app.id}/backups`, options);
1120
- if (response.statusCode !== 200) throw new Error(`Failed to list backups: ${requestError(response)}`);
1120
+ if (response.status !== 200) throw new Error(`Failed to list backups: ${requestError(response)}`);
1121
1121
  if (response.body.backups.length === 0) throw new Error('No backups');
1122
1122
 
1123
1123
  response.body.backups = response.body.backups.map(function (backup) {
@@ -1143,7 +1143,7 @@ async function restore(localOptions, cmd) {
1143
1143
 
1144
1144
  const request = createRequest('POST', `/api/v1/apps/${app.id}/restore`, options);
1145
1145
  const response = await request.send({ backupId });
1146
- if (response.statusCode !== 202) return exit(`Failed to restore app: ${requestError(response)}`);
1146
+ if (response.status !== 202) return exit(`Failed to restore app: ${requestError(response)}`);
1147
1147
 
1148
1148
  // FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
1149
1149
  await waitForFinishInstallation(app.id, response.body.taskId, options);
@@ -1202,7 +1202,7 @@ async function importApp(localOptions, cmd) {
1202
1202
  }
1203
1203
 
1204
1204
  const response = await createRequest('POST', `/api/v1/apps/${app.id}/import`, options);
1205
- if (response.statusCode !== 202) return exit(`Failed to import app: ${requestError(response)}`);
1205
+ if (response.status !== 202) return exit(`Failed to import app: ${requestError(response)}`);
1206
1206
 
1207
1207
  await waitForFinishInstallation(app.id, response.body.taskId, options);
1208
1208
  console.log('\n\nApp is restored');
@@ -1223,7 +1223,7 @@ async function exportApp(localOptions, cmd) {
1223
1223
 
1224
1224
  const request = createRequest('POST', `/api/v1/apps/${app.id}/export`, options);
1225
1225
  const response = await request.send(data);
1226
- if (response.statusCode !== 202) return exit(`Failed to export app: ${requestError(response)}`);
1226
+ if (response.status !== 202) return exit(`Failed to export app: ${requestError(response)}`);
1227
1227
 
1228
1228
  await waitForFinishInstallation(app.id, response.body.taskId, options);
1229
1229
  console.log('\n\nApp is exported');
@@ -1256,7 +1256,7 @@ async function clone(localOptions, cmd) {
1256
1256
  };
1257
1257
  const request = createRequest('POST', `/api/v1/apps/${app.id}/clone`, options);
1258
1258
  const response = await request.send(data);
1259
- if (response.statusCode !== 201) return exit(`Failed to list apps: ${requestError(response)}`);
1259
+ if (response.status !== 201) return exit(`Failed to list apps: ${requestError(response)}`);
1260
1260
 
1261
1261
  // FIXME: this should be waitForHealthCheck but the box code incorrectly modifies the installationState
1262
1262
  console.log('App cloned as id ' + response.body.id);
@@ -1317,12 +1317,12 @@ async function exec(args, localOptions, cmd) {
1317
1317
 
1318
1318
  const request = createRequest('POST', `/api/v1/apps/${app.id}/exec`, options);
1319
1319
  const response = await request.send({ cmd: args, tty, lang });
1320
- if (response.statusCode !== 200) return exit(`Failed to create exec: ${requestError(response)}`);
1320
+ if (response.status !== 200) return exit(`Failed to create exec: ${requestError(response)}`);
1321
1321
  const execId = response.body.id;
1322
1322
 
1323
1323
  async function exitWithCode() {
1324
1324
  const response2 = await createRequest('GET', `/api/v1/apps/${app.id}/exec/${execId}`, options);
1325
- if (response2.statusCode !== 200) return exit(`Failed to get exec code: ${requestError(response2)}`);
1325
+ if (response2.status !== 200) return exit(`Failed to get exec code: ${requestError(response2)}`);
1326
1326
 
1327
1327
  process.exit(response2.body.exitCode);
1328
1328
  }
@@ -1519,7 +1519,7 @@ async function setEnv(app, env, options) {
1519
1519
 
1520
1520
  const request = createRequest('POST', `/api/v1/apps/${app.id}/configure/env`, options);
1521
1521
  const response = await request.send({ env });
1522
- if (response.statusCode !== 202) return exit(`Failed to set env: ${requestError(response)}`);
1522
+ if (response.status !== 202) return exit(`Failed to set env: ${requestError(response)}`);
1523
1523
 
1524
1524
  await waitForTask(response.body.taskId, options);
1525
1525
  console.log('\n');
@@ -1532,7 +1532,7 @@ async function envSet(envVars, localOptions, cmd) {
1532
1532
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1533
1533
 
1534
1534
  const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1535
- if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1535
+ if (response.status !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1536
1536
 
1537
1537
  const env = response.body.env;
1538
1538
  envVars.forEach(envVar => {
@@ -1554,7 +1554,7 @@ async function envUnset(envNames, localOptions, cmd) {
1554
1554
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1555
1555
 
1556
1556
  const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1557
- if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1557
+ if (response.status !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1558
1558
 
1559
1559
  const env = response.body.env;
1560
1560
  envNames.forEach(name => delete env[name]);
@@ -1572,7 +1572,7 @@ async function envList(localOptions, cmd) {
1572
1572
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1573
1573
 
1574
1574
  const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1575
- if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1575
+ if (response.status !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1576
1576
 
1577
1577
  const t = new Table();
1578
1578
 
@@ -1600,7 +1600,7 @@ async function envGet(envName, localOptions, cmd) {
1600
1600
  if (!app) return exit(NO_APP_FOUND_ERROR_STRING);
1601
1601
 
1602
1602
  const response = await createRequest('GET', `/api/v1/apps/${app.id}`, options);
1603
- if (response.statusCode !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1603
+ if (response.status !== 200) return exit(`Failed to get app: ${requestError(response)}`);
1604
1604
 
1605
1605
  console.log(response.body.env[envName]);
1606
1606
  } catch (error) {
@@ -11,7 +11,7 @@ const assert = require('assert'),
11
11
  path = require('path'),
12
12
  readline = require('./readline.js'),
13
13
  safe = require('safetydance'),
14
- superagent = require('superagent'),
14
+ superagent = require('./superagent.js'),
15
15
  Table = require('easy-table');
16
16
 
17
17
  exports = module.exports = {
@@ -31,9 +31,9 @@ exports = module.exports = {
31
31
  const NO_MANIFEST_FOUND_ERROR_STRING = 'No CloudronManifest.json found';
32
32
 
33
33
  function requestError(response) {
34
- if (response.statusCode === 401) return 'Invalid token. Use cloudron appstore login again.';
34
+ if (response.status === 401) return 'Invalid token. Use cloudron appstore login again.';
35
35
 
36
- return `${response.statusCode} message: ${response.body.message || JSON.stringify(response.body)}`; // body is sometimes just a string like in 401
36
+ return `${response.status} message: ${response.body?.message || response.text || JSON.stringify(response.body)}`; // body is sometimes just a string like in 401
37
37
  }
38
38
 
39
39
  function createRequest(method, apiPath, options) {
@@ -42,7 +42,7 @@ function createRequest(method, apiPath, options) {
42
42
  let url = `${config.appStoreOrigin()}${apiPath}`;
43
43
  if (url.includes('?')) url += '&'; else url += '?';
44
44
  url += `accessToken=${token}`;
45
- const request = superagent(method, url);
45
+ const request = superagent.request(method, url);
46
46
  request.retry(3);
47
47
  request.ok(() => true);
48
48
  return request;
@@ -77,7 +77,7 @@ async function authenticate(options) { // maybe we can use options.token to vali
77
77
  config.setAppStoreToken(null);
78
78
 
79
79
  const response = await superagent.post(createUrl('/api/v1/login')).auth(email, password).send({ totpToken: options.totpToken }).ok(() => true);
80
- if (response.statusCode === 401 && response.body.message.indexOf('TOTP') !== -1) {
80
+ if (response.status === 401 && response.body.message.indexOf('TOTP') !== -1) {
81
81
  if (response.body.message === 'TOTP token missing') console.log('A 2FA TOTP Token is required for this account.');
82
82
 
83
83
  options.totpToken = await readline.question('2FA token: ', {});
@@ -88,7 +88,7 @@ async function authenticate(options) { // maybe we can use options.token to vali
88
88
  return await authenticate(options); // try again with top set
89
89
  }
90
90
 
91
- if (response.statusCode !== 200) {
91
+ if (response.status !== 200) {
92
92
  console.log('Login failed.');
93
93
 
94
94
  options.hideBanner = true;
@@ -118,7 +118,7 @@ async function info(localOptions, cmd) {
118
118
  const [id, version] = await getAppstoreId(options.appstoreId);
119
119
 
120
120
  const response = await createRequest('GET', `/api/v1/developers/apps/${id}/versions/${version}`, options);
121
- if (response.statusCode !== 200) return exit(new Error(`Failed to list versions: ${requestError(response)}`));
121
+ if (response.status !== 200) return exit(new Error(`Failed to list versions: ${requestError(response)}`));
122
122
 
123
123
  const manifest = response.body.manifest;
124
124
  console.log('id: %s', manifest.id);
@@ -134,7 +134,7 @@ async function listVersions(localOptions, cmd) {
134
134
  const [id] = await getAppstoreId(options.appstoreId);
135
135
 
136
136
  const response = await createRequest('GET', `/api/v1/developers/apps/${id}/versions`, options);
137
- if (response.statusCode !== 200) return exit(new Error(`Failed to list versions: ${requestError(response)}`));
137
+ if (response.status !== 200) return exit(new Error(`Failed to list versions: ${requestError(response)}`));
138
138
 
139
139
  if (response.body.versions.length === 0) return console.log('No versions found.');
140
140
 
@@ -214,13 +214,12 @@ async function addVersion(manifest, baseDir, options) {
214
214
  manifest.changelog = parseChangelog(changelogPath, manifest.version);
215
215
  if (!manifest.changelog) throw new Error('Bad changelog format or missing changelog for this version');
216
216
  }
217
-
218
217
  const request = createRequest('POST', `/api/v1/developers/apps/${manifest.id}/versions`, options);
219
218
  if (iconFilePath) request.attach('icon', iconFilePath);
220
- request.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
219
+ request.attach('manifest', Buffer.from(JSON.stringify(manifest)));
221
220
  const response = await request;
222
- if (response.statusCode === 409) throw new Error('This version already exists. Use --force to overwrite.');
223
- if (response.statusCode !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
221
+ if (response.status === 409) throw new Error('This version already exists. Use --force to overwrite.');
222
+ if (response.status !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
224
223
  }
225
224
 
226
225
  async function updateVersion(manifest, baseDir, options) {
@@ -260,9 +259,9 @@ async function updateVersion(manifest, baseDir, options) {
260
259
 
261
260
  const request = createRequest('PUT', `/api/v1/developers/apps/${manifest.id}/versions/${manifest.version}`, options);
262
261
  if (iconFilePath) request.attach('icon', iconFilePath);
263
- request.attach('manifest', Buffer.from(JSON.stringify(manifest)), 'manifest');
262
+ request.attach('manifest', Buffer.from(JSON.stringify(manifest)));
264
263
  const response = await request;
265
- if (response.statusCode !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
264
+ if (response.status !== 204) throw new Error(`Failed to publish version: ${requestError(response)}`);
266
265
  }
267
266
 
268
267
  async function verifyManifest(localOptions, cmd) {
@@ -334,7 +333,7 @@ async function upload(localOptions, cmd) {
334
333
 
335
334
  const [repo, tag] = manifest.dockerImage.split(':');
336
335
  const [tagError, tagResponse] = await safe(superagent.get(`https://hub.docker.com/v2/repositories/${repo}/tags/${tag}`).ok(() => true));
337
- if (tagError || tagResponse.statusCode !== 200) return exit(`Failed to find docker image in dockerhub. check https://hub.docker.com/r/${repo}/tags : ${tagError || requestError(tagResponse)}`);
336
+ if (tagError || tagResponse.status !== 200) return exit(`Failed to find docker image in dockerhub. check https://hub.docker.com/r/${repo}/tags : ${tagError || requestError(tagResponse)}`);
338
337
 
339
338
  // ensure the app is known on the appstore side
340
339
  const baseDir = path.dirname(manifestFilePath);
@@ -342,7 +341,7 @@ async function upload(localOptions, cmd) {
342
341
  const request = createRequest('POST', '/api/v1/developers/apps', options);
343
342
  request.send({ id: manifest.id });
344
343
  const response = await request;
345
- if (response.statusCode !== 409 && response.statusCode !== 201) return exit(`Failed to add app: ${requestError(response)}`); // 409 means already exists
344
+ if (response.status !== 409 && response.status !== 201) return exit(`Failed to add app: ${requestError(response)}`); // 409 means already exists
346
345
  console.log(`Uploading ${manifest.id}@${manifest.version} (dockerImage: ${manifest.dockerImage}) for testing`);
347
346
 
348
347
  const [error2] = await safe(options.force ? updateVersion(manifest, baseDir, options) : addVersion(manifest, baseDir, options));
@@ -362,12 +361,12 @@ async function submit(localOptions, cmd) {
362
361
  const manifest = result.manifest;
363
362
 
364
363
  const response = await createRequest('POST', `/api/v1/developers/apps/${manifest.id}/versions/${manifest.version}/submit`, options);
365
- if (response.statusCode === 404) {
364
+ if (response.status === 404) {
366
365
  console.log(`No version ${manifest.version} found. Please use 'cloudron apsptore upload' first`);
367
366
  return exit('Failed to submit app for review.');
368
367
  }
369
368
 
370
- if (response.statusCode !== 200) return exit(`Failed to submit app: ${requestError(response)}`);
369
+ if (response.status !== 200) return exit(`Failed to submit app: ${requestError(response)}`);
371
370
 
372
371
  console.log('App submitted for review.');
373
372
  console.log('You will receive an email when approved.');
@@ -381,7 +380,7 @@ async function revoke(localOptions, cmd) {
381
380
  console.log(`Revoking ${id}@${version}`);
382
381
 
383
382
  const response = await createRequest('POST', `/api/v1/developers/apps/${id}/versions/${version}/revoke`, options);
384
- if (response.statusCode !== 200) return exit(`Failed to revoke version: ${requestError(response)}`);
383
+ if (response.status !== 200) return exit(`Failed to revoke version: ${requestError(response)}`);
385
384
 
386
385
  console.log('version revoked.');
387
386
  }
@@ -418,7 +417,7 @@ async function approve(localOptions, cmd) {
418
417
  }
419
418
 
420
419
  const response = await createRequest('POST', `/api/v1/developers/apps/${appstoreId}/versions/${version}/approve`, options);
421
- if (response.statusCode !== 200) return exit(`Failed to approve version: ${requestError(response)}`);
420
+ if (response.status !== 200) return exit(`Failed to approve version: ${requestError(response)}`);
422
421
 
423
422
  if (options.gitPush) {
424
423
  safe.child_process.execSync(`git push --atomic origin ${defaultBranch} ${latestTag}`, { encoding: 'utf8' });
@@ -429,7 +428,7 @@ async function approve(localOptions, cmd) {
429
428
  console.log('');
430
429
 
431
430
  const response2 = await createRequest('GET', `/api/v1/developers/apps/${appstoreId}/versions/${version}`, options);
432
- if (response2.statusCode !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
431
+ if (response2.status !== 200) return exit(`Failed to list apps: ${requestError(response)}`);
433
432
 
434
433
  console.log('Changelog for forum update: ' + response2.body.manifest.forumUrl);
435
434
  console.log('');
@@ -469,18 +468,18 @@ async function notify() {
469
468
  const categoryId = categoryMatch[1];
470
469
 
471
470
  const categoryResponse = await superagent.get(`https://forum.cloudron.io/api/v3/categories/${categoryId}/topics`).set('Authorization', `Bearer ${apiToken}`).ok(() => true);
472
- if (categoryResponse.statusCode !== 200) return exit(`Unable to get topics of category: ${requestError(categoryResponse)}`);
471
+ if (categoryResponse.status !== 200) return exit(`Unable to get topics of category: ${requestError(categoryResponse)}`);
473
472
  const topic = categoryResponse.body.response.topics.find(t => t.title.includes('Package Updates'));
474
473
  if (!topic) return exit('Could not find the Package Update topic');
475
474
  const topicId = topic.tid;
476
475
 
477
476
  const pageCountResponse = await superagent.get(`https://forum.cloudron.io/api/topic/pagination/${topicId}`).set('Authorization', `Bearer ${apiToken}`).ok(() => true);
478
- if (pageCountResponse.statusCode !== 200) return exit(`Unable to get page count of topic: ${requestError(pageCountResponse)}`);
477
+ if (pageCountResponse.status !== 200) return exit(`Unable to get page count of topic: ${requestError(pageCountResponse)}`);
479
478
  const pageCount = pageCountResponse.body.pagination.pageCount;
480
479
 
481
480
  for (let page = 1; page <= pageCount; page++) {
482
481
  const pageResponse = await superagent.get(`https://forum.cloudron.io/api/topic/${topicId}?page=${page}`).set('Authorization', `Bearer ${apiToken}`).ok(() => true);
483
- if (pageResponse.statusCode !== 200) return exit(`Unable to get topics of category: ${requestError(pageResponse)}`);
482
+ if (pageResponse.status !== 200) return exit(`Unable to get topics of category: ${requestError(pageResponse)}`);
484
483
  for (const post of pageResponse.body.posts) { // post.content is html!
485
484
  if (post.content.includes(`[${manifest.version}]`)) return exit(`Version ${manifest.version} is already on the forum.\n${post.content}`);
486
485
  }
@@ -492,6 +491,6 @@ async function notify() {
492
491
  toPid: 0 // which post is this post a reply to
493
492
  };
494
493
  const postResponse = await superagent.post(`https://forum.cloudron.io/api/v3/topics/${topicId}`).set('Authorization', `Bearer ${apiToken}`).send(postData).ok(() => true);
495
- if (postResponse.statusCode !== 200) return exit(`Unable to create changelog post: ${requestError(postResponse)}`);
494
+ if (postResponse.status !== 200) return exit(`Unable to create changelog post: ${requestError(postResponse)}`);
496
495
  console.log('Posted to forum');
497
496
  }
@@ -18,19 +18,19 @@ const assert = require('assert'),
18
18
  helper = require('./helper.js'),
19
19
  manifestFormat = require('cloudron-manifestformat'),
20
20
  micromatch = require('micromatch'),
21
- readline = require('./readline.js'),
22
- superagent = require('superagent'),
23
21
  os = require('os'),
24
22
  path = require('path'),
23
+ readline = require('./readline.js'),
25
24
  safe = require('safetydance'),
26
25
  stream = require('stream/promises'),
26
+ superagent = require('./superagent.js'),
27
27
  tar = require('tar-fs'),
28
28
  url = require('url');
29
29
 
30
30
  function requestError(response) {
31
- if (response.statusCode === 401 || response.statusCode === 403) return 'Invalid token. Use cloudron build login again.';
31
+ if (response.status === 401 || response.status === 403) return 'Invalid token. Use cloudron build login again.';
32
32
 
33
- return `${response.statusCode} message: ${response.body?.message || null}`;
33
+ return `${response.status} message: ${response.body?.message || response.text || null}`;
34
34
  }
35
35
 
36
36
  // analyzes options and merges with any existing build service config
@@ -73,8 +73,8 @@ async function login(localOptions, cmd) {
73
73
  const token = options.buildServiceToken || await readline.question('Token: ', {});
74
74
 
75
75
  const response = await superagent.get(`${buildServiceConfig.url}/api/v1/profile`).query({ accessToken: token }).ok(() => true);
76
- if (response.statusCode === 401 || response.statusCode === 403) return exit(`Authentication error: ${requestError(response)}`);
77
- if (response.statusCode !== 200) return exit(`Unexpected response: ${requestError(response)}`);
76
+ if (response.status === 401 || response.status === 403) return exit(`Authentication error: ${requestError(response)}`);
77
+ if (response.status !== 200) return exit(`Unexpected response: ${requestError(response)}`);
78
78
 
79
79
  buildServiceConfig.token = token;
80
80
  config.setBuildServiceConfig(buildServiceConfig);
@@ -156,7 +156,7 @@ async function getStatus(buildId) {
156
156
  const response2 = await superagent.get(`${buildServiceConfig.url}/api/v1/builds/${buildId}`)
157
157
  .query({ accessToken: buildServiceConfig.token })
158
158
  .ok(() => true);
159
- if (response2.statusCode !== 200) throw new Error(`Failed to get status: ${requestError(response2)}`);
159
+ if (response2.status !== 200) throw new Error(`Failed to get status: ${requestError(response2)}`);
160
160
  return response2.body.status;
161
161
  }
162
162
 
@@ -278,8 +278,8 @@ async function buildRemote(manifest, sourceDir, appConfig, options) {
278
278
  .field('buildArgs', JSON.stringify(buildArgsObject))
279
279
  .attach('sourceArchive', sourceArchiveFilePath)
280
280
  .ok(() => true);
281
- if (response.statusCode === 413) return exit('Failed to build app. The app source is too large.\nPlease adjust your .dockerignore file to only include neccessary files.');
282
- if (response.statusCode !== 201) return exit(`Failed to upload app for building: ${requestError(response)}`);
281
+ if (response.status === 413) return exit('Failed to build app. The app source is too large.\nPlease adjust your .dockerignore file to only include neccessary files.');
282
+ if (response.status !== 201) return exit(`Failed to upload app for building: ${requestError(response)}`);
283
283
 
284
284
  const buildId = response.body.id;
285
285
  console.log(`BuildId: ${buildId}`);
@@ -380,7 +380,7 @@ async function push(localOptions, cmd) {
380
380
  .query({ accessToken: buildServiceConfig.token })
381
381
  .send({ dockerImageRepo: repository, dockerImageTag: tag })
382
382
  .ok(() => true);
383
- if (response.statusCode !== 201) return exit(`Failed to push: ${requestError(response)}`);
383
+ if (response.status !== 201) return exit(`Failed to push: ${requestError(response)}`);
384
384
 
385
385
  const [logsError] = await safe(followBuildLog(options.id, !!options.raw));
386
386
  if (logsError) console.log(`Failed to get logs: ${logsError.message}`);
package/src/superagent.js CHANGED
@@ -21,20 +21,30 @@ const assert = require('assert'),
21
21
  safe = require('safetydance');
22
22
 
23
23
  class Request {
24
+ #boundary;
25
+ #redirectCount;
26
+ #retryCount;
27
+ #timer;
28
+ #body;
29
+ #okFunc;
30
+ #options;
31
+ #url;
32
+
24
33
  constructor(method, url) {
25
34
  assert.strictEqual(typeof url, 'string');
26
35
 
27
- this.url = new URL(url);
28
- this.options = {
36
+ this.#url = new URL(url);
37
+ this.#options = {
29
38
  method,
30
39
  headers: {},
31
40
  signal: null // set for timeouts
32
41
  };
33
- this.okFunc = ({ status }) => status >=200 && status <= 299;
34
- this.timer = { timeout: 0, id: null, controller: null };
35
- this.retryCount = 0;
36
- this.body = null;
37
- this.redirectCount = 5;
42
+ this.#okFunc = ({ status }) => status >=200 && status <= 299;
43
+ this.#timer = { timeout: 0, id: null, controller: null };
44
+ this.#retryCount = 0;
45
+ this.#body = Buffer.alloc(0);
46
+ this.#redirectCount = 5;
47
+ this.#boundary = null; // multipart only
38
48
  }
39
49
 
40
50
  async _handleResponse(url, response) {
@@ -74,7 +84,7 @@ class Request {
74
84
  async _makeRequest(url) {
75
85
  return new Promise((resolve, reject) => {
76
86
  const proto = url.protocol === 'https:' ? https : http;
77
- const request = proto.request(url, this.options); // ClientRequest
87
+ const request = proto.request(url, this.#options); // ClientRequest
78
88
 
79
89
  request.on('error', reject); // network error, dns error
80
90
  request.on('response', async (response) => {
@@ -82,7 +92,7 @@ class Request {
82
92
  if (error) reject(error); else resolve(result);
83
93
  });
84
94
 
85
- if (this.body) request.write(this.body);
95
+ request.write(this.#body);
86
96
 
87
97
  request.end();
88
98
  });
@@ -91,24 +101,24 @@ class Request {
91
101
  async _start() {
92
102
  let error;
93
103
 
94
- for (let i = 0; i < this.retryCount+1; i++) {
95
- if (this.timer.timeout) this.timer.id = setTimeout(() => this.timer.controller.abort(), this.timer.timeout);
96
- debug(`${this.options.method} ${this.url.toString()}` + (i ? ` try ${i+1}` : ''));
104
+ for (let i = 0; i < this.#retryCount+1; i++) {
105
+ if (this.#timer.timeout) this.#timer.id = setTimeout(() => this.#timer.controller.abort(), this.#timer.timeout);
106
+ debug(`${this.#options.method} ${this.#url.toString()}` + (i ? ` try ${i+1}` : ''));
97
107
 
98
- let response, url = this.url;
99
- for (let redirects = 0; redirects < this.redirectCount+1; redirects++) {
108
+ let response, url = this.#url;
109
+ for (let redirects = 0; redirects < this.#redirectCount+1; redirects++) {
100
110
  [error, response] = await safe(this._makeRequest(url));
101
- if (error || (response.status < 300 || response.status > 399) || (this.options.method !== 'GET')) break;
111
+ if (error || (response.status < 300 || response.status > 399) || (this.#options.method !== 'GET')) break;
102
112
  url = response.url; // follow
103
113
  }
104
114
 
105
- if (!error && !this.okFunc({ status: response.status })) {
115
+ if (!error && !this.#okFunc({ status: response.status })) {
106
116
  error = new Error(`${response.status} ${http.STATUS_CODES[response.status]}`);
107
117
  Object.assign(error, response);
108
118
  }
109
119
 
110
- if (error) debug(`${this.options.method} ${this.url.toString()} ${error.message}`);
111
- if (this.timer.timeout) clearTimeout(this.timer.id);
120
+ if (error) debug(`${this.#options.method} ${this.#url.toString()} ${error.message}`);
121
+ if (this.#timer.timeout) clearTimeout(this.#timer.id);
112
122
  if (!error) return response;
113
123
  }
114
124
 
@@ -116,52 +126,52 @@ class Request {
116
126
  }
117
127
 
118
128
  set(name, value) {
119
- this.options.headers[name.toLowerCase()] = value;
129
+ this.#options.headers[name.toLowerCase()] = value;
120
130
  return this;
121
131
  }
122
132
 
123
133
  query(data) {
124
- Object.entries(data).forEach(([key, value]) => this.url.searchParams.append(key, value));
134
+ Object.entries(data).forEach(([key, value]) => this.#url.searchParams.append(key, value));
125
135
  return this;
126
136
  }
127
137
 
128
138
  redirects(count) {
129
- this.redirectCount = count;
139
+ this.#redirectCount = count;
130
140
  return this;
131
141
  }
132
142
 
133
143
  send(data) {
134
- const contentType = this.options.headers['content-type'];
144
+ const contentType = this.#options.headers['content-type'];
135
145
  if (!contentType || contentType === 'application/json') {
136
- this.options.headers['content-type'] = 'application/json';
137
- this.body = Buffer.from(JSON.stringify(data), 'utf8');
138
- this.options.headers['content-length'] = this.body.byteLength;
146
+ this.#options.headers['content-type'] = 'application/json';
147
+ this.#body = Buffer.from(JSON.stringify(data), 'utf8');
148
+ this.#options.headers['content-length'] = this.#body.byteLength;
139
149
  } else if (contentType === 'application/x-www-form-urlencoded') {
140
- this.body = Buffer.from((new URLSearchParams(data)).toString(), 'utf8');
141
- this.options.headers['content-length'] = this.body.byteLength;
150
+ this.#body = Buffer.from((new URLSearchParams(data)).toString(), 'utf8');
151
+ this.#options.headers['content-length'] = this.#body.byteLength;
142
152
  }
143
153
  return this;
144
154
  }
145
155
 
146
156
  timeout(msecs) {
147
- this.timer.controller = new AbortController();
148
- this.timer.timeout = msecs;
149
- this.options.signal = this.timer.controller.signal;
157
+ this.#timer.controller = new AbortController();
158
+ this.#timer.timeout = msecs;
159
+ this.#options.signal = this.#timer.controller.signal;
150
160
  return this;
151
161
  }
152
162
 
153
163
  retry(count) {
154
- this.retryCount = Math.max(0, count);
164
+ this.#retryCount = Math.max(0, count);
155
165
  return this;
156
166
  }
157
167
 
158
168
  ok(func) {
159
- this.okFunc = func;
169
+ this.#okFunc = func;
160
170
  return this;
161
171
  }
162
172
 
163
173
  disableTLSCerts() {
164
- this.options.rejectUnauthorized = true;
174
+ this.#options.rejectUnauthorized = true;
165
175
  return this;
166
176
  }
167
177
 
@@ -170,21 +180,36 @@ class Request {
170
180
  return this;
171
181
  }
172
182
 
173
- attach(name, filepath) { // this is only used in tests and thus simplistic
174
- const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
183
+ field(name, value) {
184
+ if (!this.#boundary) this.#boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
185
+
186
+ const partHeader = Buffer.from(`--${this.#boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n`, 'utf8');
187
+ const partData = Buffer.from(value, 'utf8');
188
+ this.#body = Buffer.concat([this.#body, partHeader, partData, Buffer.from('\r\n', 'utf8')]);
189
+
190
+ return this;
191
+ }
175
192
 
176
- const partHeader = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${name}" filename="${path.basename(filepath)}"\r\n\r\n`, 'utf8');
177
- const partData = fs.readFileSync(filepath);
178
- const partTrailer = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8');
179
- this.body = Buffer.concat([partHeader, partData, partTrailer]);
193
+ attach(name, filepathOrBuffer) { // this is only used in tests and thus simplistic
194
+ if (!this.#boundary) this.#boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
180
195
 
181
- this.options.headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
182
- this.options.headers['content-length'] = this.body.byteLength;
196
+ const filename = Buffer.isBuffer(filepathOrBuffer) ? name : path.basename(filepathOrBuffer);
197
+ const partHeader = Buffer.from(`--${this.#boundary}\r\nContent-Disposition: form-data; name="${name}" filename="${filename}"\r\n\r\n`, 'utf8');
198
+ const partData = Buffer.isBuffer(filepathOrBuffer) ? filepathOrBuffer : fs.readFileSync(filepathOrBuffer);
199
+ this.#body = Buffer.concat([this.#body, partHeader, partData, Buffer.from('\r\n', 'utf8')]);
183
200
 
184
201
  return this;
185
202
  }
186
203
 
187
204
  then(onFulfilled, onRejected) {
205
+ if (this.#boundary) {
206
+ const partTrailer = Buffer.from(`--${this.#boundary}--\r\n`, 'utf8');
207
+ this.#body = Buffer.concat([this.#body, partTrailer]);
208
+
209
+ this.#options.headers['content-type'] = `multipart/form-data; boundary=${this.#boundary}`;
210
+ this.#options.headers['content-length'] = this.#body.byteLength;
211
+ }
212
+
188
213
  this._start().then(onFulfilled, onRejected);
189
214
  }
190
215
  }