@sap/cds-dk 7.5.1 → 7.6.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.
Files changed (74) hide show
  1. package/bin/add.js +28 -24
  2. package/bin/bind.js +59 -32
  3. package/bin/build.js +1 -1
  4. package/bin/cds.js +1 -1
  5. package/bin/env.js +1 -6
  6. package/bin/import.js +2 -2
  7. package/bin/init.js +7 -3
  8. package/bin/logout.js +2 -2
  9. package/bin/migrate.js +1 -1
  10. package/bin/version.js +1 -0
  11. package/bin/watch.js +8 -1
  12. package/lib/bind/bindingUtil.js +14 -20
  13. package/lib/bind/cfKeyProvider.js +8 -10
  14. package/lib/bind/index.js +4 -2
  15. package/lib/build/buildTaskFactory.js +31 -20
  16. package/lib/build/constants.js +9 -8
  17. package/lib/build/provider/featureToggleBuildPlugin.js +1 -7
  18. package/lib/build/provider/hana/index.js +44 -9
  19. package/lib/build/provider/internalBuildTaskProvider.js +7 -5
  20. package/lib/build/provider/java/index.js +0 -6
  21. package/lib/build/provider/mtx-sidecar/index.js +5 -0
  22. package/lib/build/provider/nodejs/index.js +32 -47
  23. package/lib/build/util.js +134 -33
  24. package/lib/deploy/to-hana/hana.js +47 -77
  25. package/lib/deploy/to-hana/hdiDeployUtil.js +15 -18
  26. package/lib/deploy/to-hana/mtaUtil.js +1 -1
  27. package/lib/env/schema.js +157 -17
  28. package/lib/env/schemas/cds-rc.js +118 -0
  29. package/lib/import/common.js +18 -23
  30. package/lib/import/odata/metadataconverter/MetadataConverter.js +12 -8
  31. package/lib/import/odata/v4parser.js +67 -12
  32. package/lib/init/constants.js +2 -4
  33. package/lib/init/merge.js +2 -1
  34. package/lib/init/template/application-logging/index.js +1 -2
  35. package/lib/init/template/approuter/files/mta.yaml.hbs +2 -0
  36. package/lib/init/template/approuter/files/package.json +1 -1
  37. package/lib/init/template/approuter/files/values.yaml.hbs +8 -1
  38. package/lib/init/template/hana/files/undeploy.json +3 -2
  39. package/lib/init/template/helm/files/chart/templates/NOTES.txt +2 -0
  40. package/lib/init/template/helm/files/chart/templates/_deployment_helpers.tpl +10 -2
  41. package/lib/init/template/helm/files/content-deployment/Chart.yaml +1 -1
  42. package/lib/init/template/helm/files/content-deployment/templates/_helpers.tpl +6 -5
  43. package/lib/init/template/helm/files/content-deployment/values.yaml +1 -1
  44. package/lib/init/template/helm/files/service-instance/Chart.yaml +1 -1
  45. package/lib/init/template/helm/files/service-instance/templates/_helpers.tpl +6 -5
  46. package/lib/init/template/helm/files/service-instance/values.yaml +1 -1
  47. package/lib/init/template/helm/files/web-application/Chart.yaml +1 -1
  48. package/lib/init/template/helm/files/web-application/templates/_api.tpl +6 -5
  49. package/lib/init/template/helm/files/web-application/values.yaml +1 -1
  50. package/lib/init/template/helm/index.js +1 -12
  51. package/lib/init/template/helm-unified-runtime/index.js +3 -2
  52. package/lib/init/template/html5-repo/files/values.yaml.hbs +9 -0
  53. package/lib/init/template/multitenancy/files/package.sidecar.json.hbs +1 -2
  54. package/lib/init/template/multitenancy/index.js +3 -2
  55. package/lib/init/template/xsuaa/files/package.json.hbs +1 -2
  56. package/lib/lint/Linter.js +2 -2
  57. package/lib/lint/checks.js +0 -51
  58. package/lib/mtx/activate.js +1 -1
  59. package/lib/mtx/auth_manager.js +50 -57
  60. package/lib/mtx/download_legacy.js +1 -1
  61. package/lib/mtx/extend.js +2 -2
  62. package/lib/mtx/pull.js +1 -1
  63. package/lib/mtx/push.js +1 -1
  64. package/lib/mtx/settings_manager.js +90 -44
  65. package/lib/mtx/tenant_action.js +1 -1
  66. package/lib/mtx/util/dependencies.js +1 -1
  67. package/lib/mtx/util/legacy_ext.js +0 -1
  68. package/lib/mtx/util/logging.js +10 -8
  69. package/lib/util/axios.js +6 -19
  70. package/lib/util/cf.js +15 -10
  71. package/npm-shrinkwrap.json +104 -48
  72. package/package.json +1 -10
  73. package/lib/env/schemas/cds-rc.json +0 -101
  74. package/lib/init/template/helm-unified-runtime/lib/build.js +0 -0
@@ -2,10 +2,8 @@ const fs = require("fs");
2
2
  const os = require("os");
3
3
  const path = require("path");
4
4
  const term = require("../util/term");
5
- const commandUtil = require("../util/command");
6
5
 
7
6
  const IS_WIN = os.platform() === "win32";
8
- const { URLS } = require("../init/constants");
9
7
 
10
8
  module.exports = {
11
9
  /**
@@ -73,55 +71,6 @@ module.exports = {
73
71
  return true;
74
72
  },
75
73
 
76
- /**
77
- * Checks if VS Code is installed. If not, suggests to install it
78
- * @param {*} inputPath CDS project root
79
- * @returns boolean whether VS Code is installed
80
- */
81
- async hasVscode(inputPath, logger = console) {
82
- try {
83
- await commandUtil.spawnCommand("code", ["--version"], {
84
- cwd: inputPath,
85
- shell: IS_WIN,
86
- });
87
- } catch (err) {
88
- logger.log(`Consider installing VS Code: ${URLS.VSCODE}`);
89
- return false;
90
- }
91
- return true;
92
- },
93
-
94
- /**
95
- * Checks whether VS Code extension <extension> is installed
96
- * @param {*} extension VS Code extension
97
- * @param {*} inputPath CDS roject root
98
- * @returns boolean whether VS Code extension is installed
99
- */
100
- async hasVscodeExtension(extension, inputPath, logger = console) {
101
- try {
102
- this.linterExtensions = await commandUtil.spawnCommand(
103
- "code",
104
- ["--list-extensions", "--category", "linters"],
105
- {
106
- cwd: inputPath,
107
- shell: IS_WIN,
108
- },
109
- true
110
- );
111
- } catch (err) {
112
- // Do nothing
113
- }
114
- if (this.linterExtensions && this.linterExtensions.includes(extension)) {
115
- return true;
116
- } else {
117
- logger.log(
118
- "\n\nConsider installing the VS Code extension for ESLint:\n" +
119
- term.info(`'dbaeumer.vscode-eslint': ${URLS.VSCODE_ESLINT}\n`)
120
- );
121
- }
122
- return false;
123
- },
124
-
125
74
  /**
126
75
  * Checks ESLint config file for proper setup
127
76
  * in context of a local ESLint plugin installation
@@ -5,7 +5,7 @@ const axios = require('axios');
5
5
  const cds = require('../cds');
6
6
  const AuthManager = require('./auth_manager');
7
7
 
8
- require('../util/axios').pruneErrors().setReqTimeout();
8
+ require('../util/axios').pruneErrors();
9
9
  const { collectFileContent, collectFiles, toUnixPath } = require('./util/fs');
10
10
  const { getMessage } = require('./util/logging');
11
11
  const { handleHttpError } = require('./util/errors');
@@ -3,29 +3,32 @@ const axios = require('axios');
3
3
  const cds = require('../cds');
4
4
  const { SettingsManager } = require('./settings_manager');
5
5
  const { ParamCollection } = require('./params');
6
-
7
- require('../util/axios').pruneErrors().setReqTimeout();
8
6
  const { getMessage } = require('./util/logging');
9
7
 
8
+ require('../util/axios').pruneErrors();
9
+
10
10
  const DEBUG = cds.debug('cli');
11
+ const REASONS = { invalid_scope: /\binvalid_scope\b/ };
11
12
 
12
- function addTokenInfo(params, authData) {
13
- params.set('token', authData.access_token);
14
- if (authData.expires_in) {
15
- params.set('tokenExpirationDate', Date.now() + authData.expires_in * 1000);
16
- } else {
17
- params.delete('tokenExpirationDate');
13
+ function assign(params, authData) {
14
+ if (authData?.access_token) {
15
+ params.set('token', authData.access_token);
16
+ if (authData.expires_in) {
17
+ params.set('tokenExpirationDate', Date.now() + authData.expires_in * 1000);
18
+ } else {
19
+ params.delete('tokenExpirationDate');
20
+ }
21
+ if (authData.refresh_token) {
22
+ params.set('refreshToken', authData.refresh_token);
23
+ } else {
24
+ params.delete('refreshToken');
25
+ }
18
26
  }
19
- if (authData.refresh_token) {
20
- params.set('refreshToken', authData.refresh_token);
21
- } else {
22
- params.delete('refreshToken');
27
+ if (authData?.passcode_url) {
28
+ params.set('passcodeUrl', authData.passcode_url);
23
29
  }
24
30
  }
25
- function addUrlInfo(params, authData) {
26
- params.set('passcodeUrl', authData.passcode_url);
27
- }
28
- function* reqParams(params) {
31
+ function reqParams(method, params) {
29
32
  const url = params.get('tokenUrl');
30
33
  const d = {};
31
34
  if (params.has('subdomain')) {
@@ -44,16 +47,9 @@ function* reqParams(params) {
44
47
  d.key = params.get('key');
45
48
  }
46
49
  const data = new URLSearchParams(d).toString();
47
- yield {
48
- method: 'post',
49
- url,
50
- data
51
- };
52
- yield {
53
- method: 'get',
54
- url: `${url}?${data}`,
55
- data: undefined
56
- };
50
+ return method === 'post'
51
+ ? { url, data }
52
+ : { url: `${url}?${data}`, data: undefined };
57
53
  }
58
54
 
59
55
  async function retrieveTokenOrPasscodeUrl(params) {
@@ -61,41 +57,37 @@ async function retrieveTokenOrPasscodeUrl(params) {
61
57
  return;
62
58
  }
63
59
 
64
- let retry;
65
- let authData;
66
- do {
67
- let response;
68
- for (const { method, url, data } of reqParams(params)) {
60
+ let response, error;
61
+ reqParams: for (const method of ['post', 'get']) {
62
+ do {
63
+ const { url, data } = reqParams(method, params);
69
64
  try {
70
65
  DEBUG?.(`Getting authentication token or passcode URL from ${method.toUpperCase()} ${params.obfuscateQueryParams(url)}`);
71
66
  response = await axios[method](url, data);
72
- } catch (error) {
67
+ } catch (e) {
68
+ error = e;
73
69
  if (error.status === 404) { // may represent 405 in case of wrong method
74
- continue;
70
+ continue reqParams;
71
+ } else if (params.has('refreshToken')) {
72
+ DEBUG?.('Discarding invalid refresh token');
73
+ params.delete('refreshToken');
74
+ } else {
75
+ break reqParams;
75
76
  }
76
- throw getMessage('Failed to get authentication token', { error });
77
77
  }
78
- if (response) {
79
- break;
80
- }
81
- }
82
- if (!response) {
83
- throw 'Failed to get authentication token';
84
- }
85
- authData = response.data;
86
-
87
- retry = !authData.access_token && params.has('refreshToken') && (params.delete('refreshToken'), true);
88
- } while (retry);
89
-
90
- if (authData.access_token) {
91
- addTokenInfo(params, authData);
78
+ } while (!(response || error));
92
79
  }
93
- if (authData.passcode_url) {
94
- addUrlInfo(params, authData);
80
+ const data = response?.data ?? error?.auth;
81
+ assign(params, data);
82
+ if (!response) {
83
+ throw new Error(REASONS.invalid_scope.test(error.message)
84
+ ? 'Token has invalid scope. Check if your user has the required roles'
85
+ : `No valid passcode or token provided. Get a passcode${
86
+ params.get('passcodeUrl') ? ' from ' + params.get('passcodeUrl') : ''
87
+ } and re-run the command with '-p <passcode>'`);
95
88
  }
96
89
  }
97
90
 
98
-
99
91
  module.exports = class AuthManager {
100
92
 
101
93
  static async login(paramValues) {
@@ -105,14 +97,13 @@ module.exports = class AuthManager {
105
97
  if (params.has('username')) {
106
98
  params.set('reqAuth', { auth: { username: params.get('username'), password: params.get('password') } });
107
99
  } else if (!params.get('skipToken')) {
108
- await retrieveTokenOrPasscodeUrl(params);
109
- if (!params.has('token')) {
100
+ try {
101
+ await retrieveTokenOrPasscodeUrl(params);
102
+ } catch (error) {
110
103
  if (params.get('saveData')) {
111
104
  await SettingsManager.saveSettings(params);
112
105
  }
113
- throw `Failed to login: no valid token or passcode provided. Get a passcode${
114
- params.get('passcodeUrl') ? ' from ' + params.get('passcodeUrl') : ''
115
- } and supply the code when re-running the command`;
106
+ throw getMessage('Failed to login', { error });
116
107
  }
117
108
  params.set('reqAuth', { headers: { Authorization: 'Bearer ' + params.get('token') } });
118
109
  }
@@ -127,8 +118,10 @@ module.exports = class AuthManager {
127
118
  static async logout(paramValues) {
128
119
  SettingsManager.init();
129
120
  const params = new ParamCollection(paramValues);
121
+ await SettingsManager.setKeytar(params, true);
130
122
  if (params.get('clearInvalid')) {
131
- await SettingsManager.deleteInvalid();
123
+ await SettingsManager.deleteInvalidSettings();
124
+ await SettingsManager.deleteInvalidTokens();
132
125
  } else {
133
126
  await SettingsManager.deleteToken(params);
134
127
  if (params.get('deleteSettings')) {
@@ -4,7 +4,7 @@ const axios = require('axios');
4
4
 
5
5
  const { login } = require('./auth_manager');
6
6
 
7
- require('../util/axios').pruneErrors().setReqTimeout();
7
+ require('../util/axios').pruneErrors();
8
8
  const { handleHttpError } = require('./util/errors');
9
9
 
10
10
  const DOWNLOAD_FILE_NAME = 'migrated_projects.tgz'; // TODO try to add tenant later, if possible
package/lib/mtx/extend.js CHANGED
@@ -4,7 +4,7 @@ const axios = require('axios');
4
4
 
5
5
  const AuthManager = require('./auth_manager');
6
6
 
7
- require('../util/axios').pruneErrors().setReqTimeout();
7
+ require('../util/axios').pruneErrors();
8
8
  const { normalizePath, isParent } = require('./util/fs');
9
9
  const Question = require('./util/question');
10
10
  const { getMessage } = require('./util/logging');
@@ -163,7 +163,7 @@ module.exports = class Extend {
163
163
  }
164
164
 
165
165
  static promptTemplateOverwrite() {
166
- return Question.askBooleanQuestion(`This will overwrite extisting templates in folder ${TEMPLATE_FOLDER}. Continue (yN)? `);
166
+ return Question.askBooleanQuestion(`This will overwrite existing templates in folder ${TEMPLATE_FOLDER}. Continue (yN)? `);
167
167
  }
168
168
 
169
169
  static _logWrittenFile(file) {
package/lib/mtx/pull.js CHANGED
@@ -7,7 +7,7 @@ const axios = require('axios');
7
7
 
8
8
  const { login } = require('./auth_manager');
9
9
 
10
- require('../util/axios').pruneErrors().setReqTimeout();
10
+ require('../util/axios').pruneErrors();
11
11
  const { getMessage } = require('./util/logging');
12
12
  const { handleHttpError } = require('./util/errors');
13
13
 
package/lib/mtx/push.js CHANGED
@@ -6,7 +6,7 @@ const axios = require('axios');
6
6
  const { login } = require('./auth_manager');
7
7
  const buildSystem = require('../build')
8
8
 
9
- require('../util/axios').pruneErrors().setReqTimeout();
9
+ require('../util/axios').pruneErrors();
10
10
  const { getMessage } = require('./util/logging');
11
11
  const { schemaRegex } = require('./util/urls');
12
12
  const { handleHttpError } = require('./util/errors');
@@ -50,6 +50,7 @@ const CONFIG = {
50
50
  };
51
51
 
52
52
  let keytar;
53
+ let keytarDisabled;
53
54
 
54
55
  function other(tokenStorage) {
55
56
  if (!tokenStorage) {
@@ -104,28 +105,31 @@ async function getTokenBaseUrl(params, renewUrl) {
104
105
  return url.toString();
105
106
  };
106
107
 
107
- let existingPath;
108
- const oauthPaths = [OAUTH_PATH_STREAMLINED_MTX];
109
- for (const path of oauthPaths) {
110
- try {
111
- await axios.head(appUrlWith(path));
112
- existingPath = path;
113
- break;
114
- } catch (error) {
115
- if ([401, 404].includes(error.status)) {
116
- continue;
117
- }
108
+ let path = OAUTH_PATH_OLD_MTX;
109
+ try {
110
+ await axios.head(appUrlWith(OAUTH_PATH_STREAMLINED_MTX));
111
+ path = OAUTH_PATH_STREAMLINED_MTX;
112
+ } catch (error) {
113
+ if (![401, 404].includes(error.status)) {
118
114
  throw error;
119
115
  }
116
+ DEBUG?.(`Falling back to legacy token URL: ${error.message}`);
120
117
  }
121
- existingPath = existingPath ?? OAUTH_PATH_OLD_MTX; // REVISIT can this just be included in the array?
122
- return appUrlWith(existingPath);
118
+ return appUrlWith(path);
123
119
  }
124
120
 
125
121
  function getKeyringAccountName(params) {
126
122
  return `${params.get('appUrl')}|${params.get('subdomain')}`;
127
123
  }
128
124
 
125
+ function getAccountUrl(account) {
126
+ return account.includes('|') ? account.split('|')[0] : null;
127
+ }
128
+
129
+ function getAccountSubdomain(account) {
130
+ return account.includes('|') ? account.split('|')[1] : null;
131
+ }
132
+
129
133
  function logConfigPaths() {
130
134
  DEBUG?.(`Settings are stored in ${CONFIG.paths.settings}`);
131
135
  DEBUG?.(`Authentication data is stored in ${CONFIG.paths.auth} (${TOKEN_STORAGE_DESC.plain}) or ${CONFIG.keyringDesignation} (${TOKEN_STORAGE_DESC.keyring})`);
@@ -145,6 +149,8 @@ class SettingsManager {
145
149
 
146
150
  static init() {
147
151
  logConfigPaths();
152
+ keytar = undefined;
153
+ keytarDisabled = undefined;
148
154
  }
149
155
 
150
156
  static get config() {
@@ -173,9 +179,9 @@ class SettingsManager {
173
179
  }
174
180
 
175
181
  if (params.get('clearOtherTokenStorage')) {
176
- // Delete token from the other storage.
182
+ // Delete data from the other storage.
177
183
  params.delete('clearOtherTokenStorage');
178
- await this.deleteToken(params, true);
184
+ await this.deleteToken(params, { fromOtherStorage: true });
179
185
  }
180
186
 
181
187
  if (params.get('tokenStorage') === 'plain') {
@@ -184,7 +190,7 @@ class SettingsManager {
184
190
  await this.setKeytar(params);
185
191
  await this._saveAuthToKeyring(params, params.toEntries(Persistence.auth));
186
192
  } else {
187
- console.log('Note: the authentication token is not saved by default. To save the token for later commands, please run `cds login`.');
193
+ console.log('Note: authentication data is not persisted by default. To save tokens for later commands, please run `cds login`.');
188
194
  }
189
195
  }
190
196
 
@@ -259,7 +265,7 @@ class SettingsManager {
259
265
  }
260
266
 
261
267
  static async setKeytar(params, logout = false) {
262
- if (params.get('skipToken')) {
268
+ if (keytar || keytarDisabled || params.get('skipToken')) {
263
269
  return;
264
270
  }
265
271
  if (isBas()) {
@@ -273,7 +279,7 @@ class SettingsManager {
273
279
  if (keytar) {
274
280
  return;
275
281
  }
276
- // keytar missing
282
+ keytarDisabled = true;
277
283
 
278
284
  if (params.get('tokenStorage') === 'keyring' && params.get('saveData')) {
279
285
  // Explicit login w/o plain-text storage enabled
@@ -318,7 +324,19 @@ class SettingsManager {
318
324
  throw 'Passcode cannot be empty';
319
325
  }
320
326
  }
321
- function setRelevantAuth(auth) {
327
+ async function addAuthSettings() {
328
+ function removeObsoleteToken() {
329
+ if (params.get('renewLogin') || params.get('tokenExpirationDate') <= Date.now()) {
330
+ DEBUG?.((params.get('renewLogin') ? 'Renewing' : 'Refreshing expired') + ' authentication token');
331
+ params.delete('token');
332
+ params.delete('tokenExpirationDate');
333
+ }
334
+ }
335
+
336
+ const auth = {
337
+ plain: await SettingsManager._loadAuthFromFile(params.get('appUrl'), params.get('subdomain')),
338
+ keyring: await SettingsManager._loadAuthFromKeyring(params.get('appUrl'), params.get('subdomain'))
339
+ };
322
340
  if (!(auth.keyring.has('tokenStorage') || auth.plain.has('tokenStorage'))) {
323
341
  // Saved auth data not present.
324
342
  return;
@@ -348,12 +366,8 @@ class SettingsManager {
348
366
 
349
367
  params.merge(auth[storage]);
350
368
  }
351
- }
352
- function removeObsoleteToken() {
353
- if (params.get('renewLogin') || params.get('tokenExpirationDate') <= Date.now()) {
354
- DEBUG?.((params.get('renewLogin') ? 'Renewing' : 'Refreshing expired') + ' authentication token');
355
- params.delete('token');
356
- params.delete('tokenExpirationDate');
369
+ if (params.has('token')) {
370
+ removeObsoleteToken();
357
371
  }
358
372
  }
359
373
 
@@ -387,21 +401,13 @@ class SettingsManager {
387
401
  }
388
402
  }
389
403
 
390
- const auth = {
391
- plain: await this._loadAuthFromFile(params.get('appUrl'), params.get('subdomain')),
392
- keyring: await this._loadAuthFromKeyring(params.get('appUrl'), params.get('subdomain'))
393
- };
394
-
395
- setRelevantAuth(auth);
396
- if (params.has('token')) {
397
- removeObsoleteToken();
398
- }
404
+ await addAuthSettings();
399
405
  if (!logout && !params.has('token') && !params.has('refreshToken') && !params.has('passcode') && !params.has('clientsecret') && !params.has('key')) {
400
406
  await addPasscode();
401
407
  }
402
408
  }
403
409
 
404
- static async deleteToken(params, fromOtherStorage = false) {
410
+ static async deleteToken(params, { fromOtherStorage = false, invalid = false } = {}) {
405
411
  const allParams = params.clone();
406
412
  await this.loadAndMergeSettings(allParams, true);
407
413
 
@@ -409,11 +415,13 @@ class SettingsManager {
409
415
  let fromStorage = fromOtherStorage && other(params.get('tokenStorage'));
410
416
  const deleteBoth = !fromStorage && allParams.get('tokenStorage') && allParams.get('clearOtherTokenStorage');
411
417
 
412
- DEBUG?.(`Deleting authentication data${fromOtherStorage ? ' from other storage' : ''} for ${target}`);
418
+ DEBUG?.(`Deleting${invalid ? ' invalid' : ''} authentication data${fromOtherStorage ? ' from other storage' : ''} for ${target}`);
413
419
 
414
420
  fromStorage = fromStorage || allParams.get('tokenStorage');
415
421
  if (!fromStorage) {
416
- console.log('Failed to delete authentication data: none found for', target);
422
+ if (!invalid) {
423
+ console.log('Failed to delete authentication data: none found for', target);
424
+ }
417
425
  return;
418
426
  }
419
427
 
@@ -430,7 +438,7 @@ class SettingsManager {
430
438
  await this._saveToFile(projectFolder, null);
431
439
  }
432
440
 
433
- static async deleteInvalid() {
441
+ static async deleteInvalidSettings() {
434
442
  const settingsByFolder = await this._loadFromFile(undefined);
435
443
  const deletionFolders = [];
436
444
  const deletionUrlsAndSubdomains = new Set();
@@ -451,13 +459,31 @@ class SettingsManager {
451
459
  const subdomain = urlAndSubdomain.replace(/.*\|/, '');
452
460
  const urlReference = Object.values(settingsByFolder).find(settings => settings.appUrl === appUrl && settings.subdomain === subdomain);
453
461
  if (!urlReference) {
454
- await this.deleteToken(new ParamCollection({ appUrl, subdomain }));
462
+ await this.deleteToken(new ParamCollection({ appUrl, subdomain }), { invalid: true });
455
463
  }
456
464
  }
457
465
  console.log('Deleted settings for nonexistent project folders:', deletionFolders.map(folder => ' ' + folder));
458
466
  } else {
459
- console.log('All settings seem valid');
467
+ console.log('All saved project folders seem valid');
468
+ }
469
+ }
470
+
471
+ static async deleteInvalidTokens() {
472
+ async function deleteFrom(allAuth) {
473
+ for (const [appUrl, authForUrl = {}] of Object.entries(allAuth)) {
474
+ for (const [subdomain, auth] of Object.entries(authForUrl)) {
475
+ if (!auth.token) {
476
+ continue;
477
+ }
478
+ if (auth.tokenExpirationDate <= Date.now()) {
479
+ await SettingsManager.deleteToken(new ParamCollection({ appUrl, subdomain }), { invalid: true });
480
+ }
481
+ }
482
+ }
460
483
  }
484
+
485
+ await deleteFrom(await this._loadAuthFromFile(undefined, undefined));
486
+ await deleteFrom(await this._loadAllAuthFromKeyring());
461
487
  }
462
488
 
463
489
  static async _saveToFile(projectFolder, paramValues) {
@@ -555,11 +581,11 @@ class SettingsManager {
555
581
  return emptyData(all);
556
582
  }
557
583
 
558
- let allAuth
584
+ let allAuth;
559
585
  try {
560
586
  allAuth = JSON.parse((await fs.promises.readFile(CONFIG.paths.auth)).toString());
561
587
  } catch (err) {
562
- DEBUG?.('Empty authentication-data file');
588
+ DEBUG?.(`${capitalize(TOKEN_STORAGE_DESC.plain)} contains invalid saved auth`);
563
589
  return emptyData(all);
564
590
  }
565
591
 
@@ -568,14 +594,14 @@ class SettingsManager {
568
594
  }
569
595
 
570
596
  if (!allAuth[appUrl]) {
571
- DEBUG?.(`File contains no authentication data for app URL ${appUrl}`);
597
+ DEBUG?.(`${capitalize(TOKEN_STORAGE_DESC.plain)} contains no authentication data for app URL ${appUrl}`);
572
598
  return new ParamCollection();
573
599
  }
574
600
 
575
601
  const authForSubdomain = allAuth[appUrl][subdomain];
576
602
 
577
603
  if (!authForSubdomain) {
578
- DEBUG?.(`File contains no authentication data for subdomain ${subdomain} (app URL: ${appUrl})`);
604
+ DEBUG?.(`${capitalize(TOKEN_STORAGE_DESC.plain)} contains no authentication data for subdomain ${subdomain} (app URL: ${appUrl})`);
579
605
  return new ParamCollection();
580
606
  }
581
607
 
@@ -613,6 +639,26 @@ class SettingsManager {
613
639
  return new ParamCollection(auth);
614
640
  }
615
641
 
642
+ static async _loadAllAuthFromKeyring() {
643
+ if (!keytar) {
644
+ return {};
645
+ }
646
+
647
+ return (await keytar.findCredentials(MTX_FULLY_QUALIFIED))
648
+ .reduce((result, { account, password: authString }) => {
649
+ const appUrl = getAccountUrl(account);
650
+ const subdomain = getAccountSubdomain(account);
651
+ if (appUrl && subdomain) {
652
+ try {
653
+ (result[appUrl] ??= {})[subdomain] = JSON.parse(authString);
654
+ } catch (error) {
655
+ throw new Error(`${capitalize(TOKEN_STORAGE_DESC.keyring)} contains invalid saved auth for URL ${appUrl} and subdomain ${subdomain}`);
656
+ }
657
+ }
658
+ return result;
659
+ }, {});
660
+ }
661
+
616
662
  }
617
663
 
618
664
  module.exports = {
@@ -2,7 +2,7 @@ const cds = require('../cds');
2
2
  const { ParamCollection } = require('./params');
3
3
  const AuthManager = require('./auth_manager');
4
4
 
5
- require('../util/axios').pruneErrors().setReqTimeout();
5
+ require('../util/axios').pruneErrors();
6
6
  const { getMessage } = require('./util/logging');
7
7
  const { capitalize } = require('./util/strings');
8
8
  const { handleHttpError } = require('./util/errors');
@@ -24,7 +24,7 @@ function requireGlobal(packageId) {
24
24
  return require(packagePath);
25
25
  } catch (error) {
26
26
  // Ignore error.
27
- DEBUG?.(`Failed to find module '${packageId}' in ${globalModulesDir()}`, error);
27
+ DEBUG?.(`Failed to find module '${packageId}' in ${globalModulesDir()}:`, error);
28
28
  }
29
29
  }
30
30
 
@@ -2,7 +2,6 @@ const path = require('path');
2
2
  const fs = require('fs').promises;
3
3
 
4
4
  const { concatUrls } = require('./urls');
5
- require('../../util/axios').pruneErrors().setReqTimeout();
6
5
 
7
6
  const MTX_URL_PATH_PREFIX = 'mtx/v1/model/';
8
7
 
@@ -1,15 +1,17 @@
1
1
  /* eslint-disable no-console */
2
2
 
3
3
  function getMessage(errorText, { error, command, help = command && getCommandHelp(command) } = {}) {
4
- let message = errorText ? errorText : '';
4
+ let message = errorText ? errorText.trim() : '';
5
5
  if (error) {
6
- message += '\n' + (
7
- error.response?.data?.message // response body == error
8
- || error.response?.data?.error?.message // response body == { error }
9
- || error.message // no response attached
10
- );
11
- if (error.stack && require('../../cds').debug('cli')) {
12
- message += '\n' + error.stack;
6
+ const includeStack = error.stack && require('../../cds').debug('cli');
7
+ message = message.replace(/(?<!\p{P})$/u, ':'); // Append colon if no punctuation at the end
8
+ message += includeStack ? '\n' : ' ';
9
+ const oriMsg = error.response?.data?.message // case: response body == error
10
+ || error.response?.data?.error?.message // case: response body == { error }
11
+ || (includeStack ? '' : error.message);
12
+ message += oriMsg;
13
+ if (includeStack) {
14
+ message += (oriMsg ? '\n' : '') + error.stack;
13
15
  }
14
16
  }
15
17
  if (typeof help === 'string') {
package/lib/util/axios.js CHANGED
@@ -1,25 +1,7 @@
1
1
  module.exports = {
2
- pruneErrors,
3
- setReqTimeout
2
+ pruneErrors
4
3
  };
5
4
 
6
- // Will only count the time until the server starts responding, not the time until the request is finished.
7
- const GENERIC_REQUEST_TIMEOUT_MS = 1500;
8
-
9
- function setReqTimeout() {
10
- require('axios').interceptors.request.use(
11
- config => {
12
- config.timeout = GENERIC_REQUEST_TIMEOUT_MS;
13
- return config;
14
- },
15
- error => {
16
- return Promise.reject(error)
17
- }
18
- );
19
- return module.exports;
20
- }
21
-
22
-
23
5
  function pruneErrors() {
24
6
  function stacklessError(message) {
25
7
  const { stackTraceLimit } = Error;
@@ -72,6 +54,11 @@ function pruneErrors() {
72
54
  if (status) {
73
55
  error.status = status;
74
56
  }
57
+ if (data?.passcode_url) {
58
+ error.auth = {
59
+ passcode_url: data.passcode_url
60
+ };
61
+ }
75
62
  }
76
63
 
77
64
  if (require('../').debug('req')) {