@sap/cli-core 2024.22.0 → 2024.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,77 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## 2024.24.0
9
+
10
+ ### Added
11
+
12
+ - Support for Node v22.
13
+
14
+ - New command `<cli> config secrets check` has been added to the CLI. This command allows you to verify whether the secrets file used to run commands is consistent.
15
+
16
+ Examples for inconsistent secrets files:
17
+
18
+ ```bash
19
+ $ datasphere config secrets check
20
+ ✔ Please enter the path to the secrets file: … secrets.json
21
+ the provided secrets file is not consistent. the secrets file is missing either property access_token or properties refresh_token, client_id and client_secret
22
+ Failed to check secrets file consistency
23
+ ```
24
+
25
+ ```bash
26
+ $ datasphere config secrets check
27
+ ✔ Please enter the path to the secrets file: … secrets.json
28
+ the provided secrets file is not consistent. the secrets file is missing either property tenantUrl or properties authorization_url and token_url
29
+ Failed to check secrets file consistency
30
+ ```
31
+
32
+ Example for consistent secrets file:
33
+
34
+ ```bash
35
+ $ datasphere config secrets check
36
+ ✔ Please enter the path to the secrets file: … secrets.json
37
+ the secrets file is consistent
38
+ ```
39
+
40
+ - Support for the authorization flow `client_credentials`. When logging in, users can now explicitly use the `client_credentials` flow. The help shows the available option to specify the authorization flow.
41
+
42
+ ```bash
43
+ % datasphere login --help
44
+ Usage: datasphere login [options]
45
+
46
+ log in to your account using interactive OAuth authentication
47
+
48
+ Options:
49
+ -H, --host <host> specifies the url where the tenant is hosted (optional)
50
+ -A, --authorization-url <url> authorization url for interactive oauth session authentication (optional)
51
+ -t, --token-url <url> token url for interactive oauth session authentication (optional)
52
+ -c, --client-id <id> client id for interactive oauth session authentication (optional)
53
+ -C, --client-secret <secret> client secret for interactive oauth session authentication (optional)
54
+ -a, --access-token <token> access token for interactive oauth session authentication (optional)
55
+ -b, --code <code> code for oauth token retrieval (optional)
56
+ -r, --refresh-token <token> refresh token for interactive oauth session authentication (optional)
57
+ -s, --secrets-file <file> path to secrets file (optional)
58
+ -B, --browser <browser> specifies the browser to open (optional) (choices: "chrome", "firefox", "edge", default: "chrome")
59
+ -d, --authorization-flow <authorization-flow> specifies the authorization flow to use (optional) (choices: "authorization_code", "client_credentials", default: "authorization_code")
60
+ -h, --help display help for command
61
+
62
+ Only command-specific options are listed here. To learn more about available generic options, visit https://tinyurl.com/yck8vv4w
63
+ ```
64
+
65
+ There's a new option `-d, --authorization-flow <authorization-flow> specifies the authorization flow to use (optional) (choices: "authorization_code", "client_credentials", default: "authorization_code")`.
66
+
67
+ By default, the authorization flow `authorization_code` is used which was also the implicit default before. If users create a technical OAuth client, they must set the authorization flow to `client_credentials` explicitly.
68
+
69
+ `% datasphere login --authorization-flow client_credentials ...`
70
+
71
+ - When using option `--secrets-file` and the file containing the secrets does not exist or is not in a valid JSON format, the CLI now shows an error to the user: `The secrets file at location /path/to/secrets.json could not be read and JSON.parse'd. Does the file exist and is it a valid JSON file?`
72
+
73
+ ## 2024.22.0
74
+
75
+ ### Fixed
76
+
77
+ - Issues with option `--input` which would not set the request body correctly. As a workaround, option `--file-path` can be used.
78
+
8
79
  ## 2024.17.0
9
80
 
10
81
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Command-Line Interface (CLI) Core Module.
4
4
 
5
- [![Node Version](https://img.shields.io/badge/node-18.xx.x-brightgreen)](https://nodejs.org/dist/latest-v18.x/docs/api/#) [![Node Version](https://img.shields.io/badge/node-19.xx.x-brightgreen)](https://nodejs.org/dist/latest-v19.x/docs/api/#) [![Node Version](https://img.shields.io/badge/node-20.xx.x-brightgreen)](https://nodejs.org/dist/latest-v20.x/docs/api/#) [![Node Version](https://img.shields.io/badge/node-21.xx.x-brightgreen)](https://nodejs.org/dist/latest-v21.x/docs/api/#) [![npm version](https://badge.fury.io/js/@sap%2Fcli-core.svg)](https://badge.fury.io/js/@sap%2Fcli-core)
5
+ [![Node Version](https://img.shields.io/badge/node-18.xx.x-brightgreen)](https://nodejs.org/dist/latest-v18.x/docs/api/#) [![Node Version](https://img.shields.io/badge/node-19.xx.x-brightgreen)](https://nodejs.org/dist/latest-v19.x/docs/api/#) [![Node Version](https://img.shields.io/badge/node-20.xx.x-brightgreen)](https://nodejs.org/dist/latest-v20.x/docs/api/#) [![Node Version](https://img.shields.io/badge/node-21.xx.x-brightgreen)](https://nodejs.org/dist/latest-v21.x/docs/api/#) [![Node Version](https://img.shields.io/badge/node-22.xx.x-brightgreen)](https://nodejs.org/dist/latest-v22.x/docs/api/#) [![npm version](https://badge.fury.io/js/@sap%2Fcli-core.svg)](https://badge.fury.io/js/@sap%2Fcli-core)
6
6
 
7
7
  ## Content
8
8
 
@@ -1,15 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SecretsStorageImpl = void 0;
4
- const config_1 = require("../../config");
5
4
  const constants_1 = require("../../constants");
6
5
  const logger_1 = require("../../logger");
7
6
  const utils_1 = require("../../logger/utils");
8
7
  const types_1 = require("../../types");
9
- const http_1 = require("../../utils/http");
10
- const utils_2 = require("../../utils/utils");
11
8
  const cache_1 = require("../cache");
12
- const utils_3 = require("./utils");
9
+ const utils_2 = require("./utils");
13
10
  class SecretsStorageImpl {
14
11
  secrets = [];
15
12
  logger;
@@ -24,7 +21,7 @@ class SecretsStorageImpl {
24
21
  for (let i = 0; i < this.secrets.length; i++) {
25
22
  this.secrets[i].id = i;
26
23
  if (this.secrets[i].client_id) {
27
- this.secrets[i].customClient = (0, utils_3.isCustomClient)(this.secrets[i].client_id);
24
+ this.secrets[i].customClient = (0, utils_2.isCustomClient)(this.secrets[i].client_id);
28
25
  // eslint-disable-next-line no-await-in-loop
29
26
  await this.updateUrls(i);
30
27
  }
@@ -52,7 +49,7 @@ class SecretsStorageImpl {
52
49
  return this.getSecretById(await this.getDefaultSecretId());
53
50
  }
54
51
  async getDefaultSecretId() {
55
- const tenantUrl = (0, utils_3.getTenantUrl)();
52
+ const tenantUrl = (0, utils_2.getTenantUrl)();
56
53
  const { host } = new URL(tenantUrl);
57
54
  const secret = this.secrets.find((s) => new URL(s.tenantUrl).host === host);
58
55
  if (!secret) {
@@ -76,7 +73,7 @@ class SecretsStorageImpl {
76
73
  this.secrets.splice(id, 1);
77
74
  }
78
75
  updateSecret(secret) {
79
- const tenantUrl = (0, utils_3.getTenantUrl)();
76
+ const tenantUrl = (0, utils_2.getTenantUrl)();
80
77
  let prevSecretIx = this.secrets.findIndex((s) => s.tenantUrl === tenantUrl);
81
78
  let newSecret;
82
79
  if (prevSecretIx > -1) {
@@ -96,42 +93,7 @@ class SecretsStorageImpl {
96
93
  return prevSecretIx;
97
94
  }
98
95
  async updateUrls(secretIndex) {
99
- const secret = this.secrets[secretIndex];
100
- if (secret.client_id) {
101
- const config = (0, config_1.get)();
102
- let oauth = {};
103
- if (!secret.customClient &&
104
- (!secret.authorization_url || !secret.token_url)) {
105
- oauth = (await (0, http_1.fetch)({
106
- method: "GET",
107
- url: `${config.tenantUrl}/oauth`,
108
- })).data;
109
- }
110
- let authorizationUrl = secret.authorization_url;
111
- if (!authorizationUrl) {
112
- if (!secret.customClient) {
113
- authorizationUrl = oauth.authorizationUrl;
114
- }
115
- else {
116
- authorizationUrl = config.authorizationUrl;
117
- }
118
- }
119
- let tokenUrl = secret.token_url;
120
- if (!tokenUrl) {
121
- if (!secret.customClient) {
122
- tokenUrl = oauth.tokenUrl;
123
- }
124
- else {
125
- tokenUrl = config.tokenUrl;
126
- }
127
- }
128
- if (!tokenUrl || !authorizationUrl) {
129
- throw new Error("invalid token url or authorization url");
130
- }
131
- this.secrets[secretIndex].authorization_url =
132
- (0, utils_2.removeQueryParametersFromUrl)(authorizationUrl);
133
- this.secrets[secretIndex].token_url = (0, utils_2.removeQueryParametersFromUrl)(tokenUrl);
134
- }
96
+ this.secrets[secretIndex] = await (0, utils_2.updateUrls)(this.secrets[secretIndex]);
135
97
  }
136
98
  async storeSecret(secret) {
137
99
  this.updateSecret(secret);
@@ -143,8 +105,9 @@ class SecretsStorageImpl {
143
105
  }
144
106
  removeInconsistentSecrets() {
145
107
  this.secrets = this.secrets.filter((secret) => {
146
- if (!(0, utils_3.isSecretConsistent)(secret)) {
147
- this.logger.warn(`secret ${JSON.stringify(secret)} is not consistent and removed from cache`);
108
+ const consistent = (0, utils_2.isSecretConsistent)(secret);
109
+ if (!consistent.consistent) {
110
+ this.logger.warn(`secret ${JSON.stringify(secret)} is not consistent and removed from cache. ${consistent.errors.join(". ")}`);
148
111
  return false;
149
112
  }
150
113
  return true;
@@ -1,4 +1,10 @@
1
1
  import { Secret } from "../../types";
2
2
  export declare const isCustomClient: (clientId?: string) => boolean;
3
3
  export declare const getTenantUrl: () => string;
4
- export declare function isSecretConsistent(secret: Secret): boolean;
4
+ export declare function updateUrls(secret: Secret): Promise<Secret>;
5
+ export declare function isSecretConsistent(secret: Secret): {
6
+ consistent: true;
7
+ } | {
8
+ consistent: false;
9
+ errors: Array<string>;
10
+ };
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getTenantUrl = exports.isCustomClient = void 0;
4
+ exports.updateUrls = updateUrls;
4
5
  exports.isSecretConsistent = isSecretConsistent;
5
6
  const config_1 = require("../../config");
6
7
  const constants_1 = require("../../constants");
8
+ const types_1 = require("../../types");
9
+ const http_1 = require("../../utils/http");
10
+ const utils_1 = require("../../utils/utils");
7
11
  // Pre-delivered OAuth Client ID: 5a638330-5899-366e-ac00-ab62cc32dcda
8
12
  // Custom OAuth Client ID: sb-00bb7bc2-cc32-423c-921c-2abdee11a29d!b49931|client!b3650
9
13
  const isCustomClient = (clientId) => clientId ? clientId.startsWith("sb-") : true;
@@ -17,10 +21,64 @@ const getTenantUrl = () => {
17
21
  return tenantUrl;
18
22
  };
19
23
  exports.getTenantUrl = getTenantUrl;
24
+ async function updateUrls(secret) {
25
+ if (secret.client_id) {
26
+ const config = (0, config_1.get)();
27
+ let oauth = {};
28
+ if (!secret.customClient &&
29
+ (!secret.authorization_url || !secret.token_url)) {
30
+ oauth = (await (0, http_1.fetch)({
31
+ method: "GET",
32
+ url: `${config.tenantUrl}/oauth`,
33
+ })).data;
34
+ }
35
+ let authorizationUrl = secret.authorization_url;
36
+ if (!authorizationUrl) {
37
+ if (!secret.customClient) {
38
+ authorizationUrl = oauth.authorizationUrl;
39
+ }
40
+ else {
41
+ authorizationUrl = config.authorizationUrl;
42
+ }
43
+ }
44
+ let tokenUrl = secret.token_url;
45
+ if (!tokenUrl) {
46
+ if (!secret.customClient) {
47
+ tokenUrl = oauth.tokenUrl;
48
+ }
49
+ else {
50
+ tokenUrl = config.tokenUrl;
51
+ }
52
+ }
53
+ if (!tokenUrl || !authorizationUrl) {
54
+ throw new Error("invalid token url or authorization url");
55
+ }
56
+ // eslint-disable-next-line no-param-reassign
57
+ secret.authorization_url = (0, utils_1.removeQueryParametersFromUrl)(authorizationUrl);
58
+ // eslint-disable-next-line no-param-reassign
59
+ secret.token_url = (0, utils_1.removeQueryParametersFromUrl)(tokenUrl);
60
+ }
61
+ return secret;
62
+ }
20
63
  function isSecretConsistent(secret) {
21
- // all these properties must exist
22
- return ((!!secret.tenantUrl ||
23
- (!!secret.authorization_url && !!secret.token_url)) &&
24
- (!!secret.access_token ||
25
- (!!secret.refresh_token && !!secret.client_id && !!secret.client_secret)));
64
+ const errors = [];
65
+ if (!((0, utils_1.isValidURL)(secret.tenantUrl) ||
66
+ ((secret.authorization_flow !== types_1.GrantType.authorization_code ||
67
+ (0, utils_1.isValidURL)(secret.authorization_url)) &&
68
+ (0, utils_1.isValidURL)(secret.token_url)))) {
69
+ errors.push("the secrets file is missing either property tenantUrl or properties authorization_url and token_url" +
70
+ " or the URLs are not valid");
71
+ }
72
+ if (!(secret.access_token ||
73
+ ((secret.authorization_flow !== types_1.GrantType.authorization_code ||
74
+ secret.refresh_token) &&
75
+ secret.client_id &&
76
+ secret.client_secret))) {
77
+ errors.push("the secrets file is missing either property access_token or properties refresh_token, " +
78
+ "client_id and client_secret");
79
+ }
80
+ if (errors.length > 0) {
81
+ return { consistent: false, errors };
82
+ }
83
+ return { consistent: true };
26
84
  }
@@ -0,0 +1,4 @@
1
+ import { Command, Handler } from "../../../types";
2
+ export declare const create: () => Handler;
3
+ declare const refreshCommand: Command;
4
+ export default refreshCommand;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.create = void 0;
4
+ const constants_1 = require("../../../constants");
5
+ const handler_1 = require("../../handler");
6
+ const file_1 = require("../../handler/authentication/oauth/secretsProvider/file");
7
+ const logger_1 = require("../../../logger");
8
+ /* jscpd:ignore-end */
9
+ const create = () => async () => async () => {
10
+ const { output } = (0, logger_1.get)("commands.secrets.check");
11
+ await (0, file_1.readSecretsFile)();
12
+ output("the secrets file is consistent");
13
+ };
14
+ exports.create = create;
15
+ const refreshCommand = {
16
+ type: "command",
17
+ command: "check",
18
+ description: "check secrets file consistency",
19
+ handler: (0, handler_1.createNextHandler)("commands.config.secrets.check", (0, handler_1.createParseArgumentsHandler)(), (0, handler_1.createOptionsHandler)([
20
+ { ...constants_1.OPTION_SECRETS_FILE, required: true, hidden: false },
21
+ ]), (0, exports.create)()),
22
+ };
23
+ exports.default = refreshCommand;
@@ -6,10 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const refresh_command_1 = __importDefault(require("./refresh.command"));
7
7
  const reset_command_1 = __importDefault(require("./reset.command"));
8
8
  const show_command_1 = __importDefault(require("./show.command"));
9
+ const check_command_1 = __importDefault(require("./check.command"));
9
10
  const secretsCommand = {
10
11
  type: "topCommand",
11
12
  command: "secrets",
12
13
  description: "work with the locally stored secrets",
13
- subCommands: [show_command_1.default, refresh_command_1.default, reset_command_1.default],
14
+ subCommands: [show_command_1.default, refresh_command_1.default, reset_command_1.default, check_command_1.default],
14
15
  };
15
16
  exports.default = secretsCommand;
@@ -16,6 +16,6 @@ const core_1 = require("../../../config/core");
16
16
  const succeed_1 = require("../succeed");
17
17
  exports.create = process.env.SUPPORT === "true"
18
18
  ? technicalJWT_1.create
19
- : () => (0, next_1.create)("commands.handler.authentication", (0, resilient_1.create)((0, next_1.create)("commands.handler.authentication$oauth", (0, cache_1.create)(), (0, refreshToken_1.create)())), (0, options_1.create)(constants_1.OPTION_CLIENT_ID), (0, options_1.create)(constants_1.OPTION_CLIENT_SECRET), (0, options_1.create)(constants_1.OPTION_ACCESS_TOKEN), (0, options_1.create)(constants_1.OPTION_REFRESH_TOKEN), (0, options_1.create)(constants_1.OPTION_CODE), (0, options_1.create)(constants_1.OPTION_TOKEN_URL), (0, options_1.create)(constants_1.OPTION_AUTHORIZATION_URL), (0, core_1.getAuthenticationMethods)().includes(constants_1.AuthenticationMethod.passcode)
19
+ : () => (0, next_1.create)("commands.handler.authentication", (0, resilient_1.create)((0, next_1.create)("commands.handler.authentication$oauth", (0, cache_1.create)(), (0, refreshToken_1.create)())), (0, options_1.create)(constants_1.OPTION_CLIENT_ID), (0, options_1.create)(constants_1.OPTION_CLIENT_SECRET), (0, options_1.create)(constants_1.OPTION_ACCESS_TOKEN), (0, options_1.create)(constants_1.OPTION_REFRESH_TOKEN), (0, options_1.create)(constants_1.OPTION_CODE), (0, options_1.create)(constants_1.OPTION_TOKEN_URL), (0, options_1.create)(constants_1.OPTION_AUTHORIZATION_URL), (0, options_1.create)(constants_1.OPTION_AUTHORIZATION_FLOW), (0, core_1.getAuthenticationMethods)().includes(constants_1.AuthenticationMethod.passcode)
20
20
  ? (0, options_1.create)(constants_1.OPTION_PASSCODE)
21
21
  : (0, succeed_1.create)(), (0, options_1.create)(constants_1.OPTION_SECRETS_FILE), (0, or_1.create)("commands.handler.authentication$handler", (0, setAuthorization_1.create)(), (0, passcode_1.create)(), (0, oauth_1.create)()));
@@ -1,2 +1,3 @@
1
- import { Handler } from "../../../../../types";
1
+ import { Handler, Secret } from "../../../../../types";
2
+ export declare function readSecretsFile(): Promise<Secret>;
2
3
  export declare const create: () => Handler;
@@ -1,25 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.create = void 0;
4
+ exports.readSecretsFile = readSecretsFile;
4
5
  const fs_extra_1 = require("fs-extra");
5
6
  const next_1 = require("../../../next");
6
7
  const constants_1 = require("../../../../../constants");
7
8
  const logger_1 = require("../../../../../logger");
9
+ const config_1 = require("../../../../../config");
8
10
  const options_1 = require("../../../../../utils/options");
9
11
  const options_2 = require("../../../options");
10
12
  const SecretsStorageSingleton_1 = require("../../../../../cache/secrets/SecretsStorageSingleton");
11
- const handler = async () => async () => {
12
- const { error, debug, info } = (0, logger_1.get)("commands.handler.authentication.oauth.secretsProvider.file");
13
- info("reading secrets from file");
13
+ const utils_1 = require("../../../../../cache/secrets/utils");
14
+ async function readSecretsFile() {
15
+ const { output, error, debug } = (0, logger_1.get)("commands.handler.authentication.oauth.secretsProvider.file.readSecretsFile");
16
+ const secretsFile = (0, options_1.getOptionValueFromConfig)(constants_1.OPTION_SECRETS_FILE);
17
+ const config = (0, config_1.get)();
14
18
  try {
15
- const secretsFile = (0, options_1.getOptionValueFromConfig)(constants_1.OPTION_SECRETS_FILE);
16
- await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.storeSecret(JSON.parse(await (0, fs_extra_1.readFile)(secretsFile, "utf8")));
19
+ let content = JSON.parse(await (0, fs_extra_1.readFile)(secretsFile, "utf8"));
20
+ content.tenantUrl = config.tenantUrl ?? content.tenantUrl;
21
+ content.customClient = (0, utils_1.isCustomClient)(content.client_id);
22
+ content = await (0, utils_1.updateUrls)(content);
17
23
  debug("secrets found from file");
24
+ const consistent = (0, utils_1.isSecretConsistent)(content);
25
+ if (!consistent.consistent) {
26
+ output(`the provided secrets file is not consistent. ${consistent.errors.join(". ")}`);
27
+ throw new Error("inconsistent secrets file");
28
+ }
29
+ return content;
18
30
  }
19
31
  catch (err) {
20
32
  error("failed to read secrets from file", err);
33
+ output(`The secrets file at location ${secretsFile} could not be read and JSON.parse'd.` +
34
+ `Does the file exist and is it a valid JSON file?`);
21
35
  throw err;
22
36
  }
37
+ }
38
+ const handler = async () => async () => {
39
+ const { info } = (0, logger_1.get)("commands.handler.authentication.oauth.secretsProvider.file");
40
+ info("reading secrets from file");
41
+ const content = await readSecretsFile();
42
+ await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.storeSecret(content);
23
43
  };
24
44
  const create = () => (0, next_1.create)("commands.handler.authentication.oauth.secretsProvider.file", (0, options_2.create)([constants_1.OPTION_SECRETS_FILE]), handler);
25
45
  exports.create = create;
@@ -20,6 +20,7 @@ const setOAuthFromOptions = async () => async () => {
20
20
  token_url: (0, options_2.getOptionValueFromConfigGracefully)(constants_1.OPTION_TOKEN_URL),
21
21
  access_token: (0, options_2.getOptionValueFromConfigGracefully)(constants_1.OPTION_ACCESS_TOKEN),
22
22
  refresh_token: (0, options_2.getOptionValueFromConfigGracefully)(constants_1.OPTION_REFRESH_TOKEN),
23
+ authorization_flow: (0, options_2.getOptionValueFromConfigGracefully)(constants_1.OPTION_AUTHORIZATION_FLOW),
23
24
  expires_in: expiresIn ? parseInt(expiresIn, 10) : undefined,
24
25
  });
25
26
  };
@@ -32,5 +33,6 @@ const create = () => (0, next_1.create)("handler.authentication.oauth.secretsPro
32
33
  constants_1.OPTION_TOKEN_URL,
33
34
  constants_1.OPTION_REFRESH_TOKEN,
34
35
  constants_1.OPTION_EXPIRES_IN,
36
+ constants_1.OPTION_AUTHORIZATION_FLOW,
35
37
  ]))), setOAuthFromOptions);
36
38
  exports.create = create;
@@ -5,6 +5,7 @@ const next_1 = require("../../../next");
5
5
  const options_1 = require("../../../options");
6
6
  const constants_1 = require("../../../../../constants");
7
7
  const logger_1 = require("../../../../../logger");
8
+ const types_1 = require("../../../../../types");
8
9
  const utils_1 = require("../utils");
9
10
  const utils_2 = require("./utils");
10
11
  const SecretsStorageSingleton_1 = require("../../../../../cache/secrets/SecretsStorageSingleton");
@@ -15,12 +16,17 @@ const handler = async () => async () => {
15
16
  const secrets = await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.getDefaultSecret();
16
17
  if (!secrets.access_token && !secrets.refresh_token) {
17
18
  debug("access token not available, retrieving token from server");
18
- const code = await (0, utils_2.getCode)(secrets.authorization_url, secrets.client_id);
19
- debug("code received, reading token");
20
- await (0, utils_1.readToken)({
21
- code,
22
- grant_type: "authorization_code",
23
- });
19
+ if (secrets.authorization_flow === types_1.GrantType.authorization_code) {
20
+ const code = await (0, utils_2.getCode)(secrets.authorization_url, secrets.client_id);
21
+ debug("code received, reading token");
22
+ await (0, utils_1.readToken)({ code, grant_type: secrets.authorization_flow });
23
+ }
24
+ else if (secrets.authorization_flow === types_1.GrantType.client_credentials) {
25
+ await (0, utils_1.readToken)({ grant_type: secrets.authorization_flow });
26
+ }
27
+ else {
28
+ throw new Error(`invalid grant type ${secrets.authorization_flow}`);
29
+ }
24
30
  }
25
31
  else if (secrets.access_token) {
26
32
  debug("token available");
@@ -11,21 +11,29 @@ const options_1 = require("../../../../../utils/options");
11
11
  const SecretsStorageSingleton_1 = require("../../../../../cache/secrets/SecretsStorageSingleton");
12
12
  const openUtils_1 = require("../../../../../utils/openUtils");
13
13
  const utils_2 = require("../../../../../logger/utils");
14
+ const types_1 = require("../../../../../types");
14
15
  const getLogger = () => (0, logger_1.get)("commands.handler.authentication.oauth.tokenProvider.utils.refreshToken");
15
16
  const refreshToken = async (forceRefresh = false) => {
16
17
  const logger = getLogger();
17
18
  const secrets = await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.getDefaultSecret();
18
- if ((!forceRefresh && !secrets.expires_after) || !secrets.refresh_token) {
19
+ if ((!forceRefresh && !secrets.expires_after) ||
20
+ (secrets.authorization_flow === types_1.GrantType.authorization_code &&
21
+ !secrets.refresh_token)) {
19
22
  (0, utils_2.logVerbose)(logger, "Access token cannot be refreshed. Is the refresh token available?");
20
23
  throw new Error("invalid secrets information");
21
24
  }
22
25
  logger.info("checking token expiry date");
23
26
  if (forceRefresh || (0, utils_1.isExpired)(secrets.expires_after)) {
24
27
  logger.debug("access token is expired, refreshing token");
25
- await (0, utils_1.readToken)({
26
- refresh_token: secrets.refresh_token,
27
- grant_type: "refresh_token",
28
- });
28
+ if (secrets.authorization_flow === types_1.GrantType.client_credentials) {
29
+ await (0, utils_1.readToken)({ grant_type: secrets.authorization_flow });
30
+ }
31
+ else {
32
+ await (0, utils_1.readToken)({
33
+ refresh_token: secrets.refresh_token,
34
+ grant_type: types_1.GrantType.refresh_token,
35
+ });
36
+ }
29
37
  await (0, setAuthorization_1.updateAuthorization)();
30
38
  }
31
39
  else {
@@ -1,9 +1,12 @@
1
+ import { GrantType } from "../../../../types";
1
2
  type Data = {
2
- grant_type: "refresh_token";
3
+ grant_type: GrantType.refresh_token;
3
4
  refresh_token: string;
4
5
  } | {
5
- grant_type: "authorization_code";
6
+ grant_type: GrantType.authorization_code;
6
7
  code: string;
8
+ } | {
9
+ grant_type: GrantType.client_credentials;
7
10
  };
8
11
  export declare const calculateExpiresAfter: (expires_in: number) => number;
9
12
  export declare const isExpired: (expires_after: number) => boolean;
@@ -45,6 +45,7 @@ const readToken = async (data) => {
45
45
  })).data;
46
46
  logger.debug(`token received: ${JSON.stringify(info, null, 2)}`);
47
47
  await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.storeSecret({
48
+ ...secrets,
48
49
  ...info,
49
50
  expires_after: (0, exports.calculateExpiresAfter)(info.expires_in),
50
51
  });
@@ -11,6 +11,7 @@ const config_1 = require("../../../../config");
11
11
  const logger_1 = require("../../../../logger");
12
12
  const http_1 = require("../../../../utils/http");
13
13
  const types_1 = require("./types");
14
+ const types_2 = require("../../../../types");
14
15
  const getLogger = () => (0, logger_1.get)("commands.handler.authentication.technicalJWT.utils");
15
16
  // eslint-disable-next-line @typescript-eslint/no-var-requires, import/extensions
16
17
  const cf = require("./cf.js");
@@ -56,7 +57,7 @@ const getTechnicalJwt = async () => {
56
57
  url: `${secret.uaaUrl}/oauth/token`,
57
58
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
58
59
  data: new url_1.URLSearchParams({
59
- grant_type: "client_credentials",
60
+ grant_type: types_2.GrantType.client_credentials,
60
61
  response_type: "token",
61
62
  client_id: secret.clientid,
62
63
  client_secret: secret.clientsecret,
@@ -18,7 +18,7 @@ const REDIRECT_WITHOUT_CHANGE = [307, 308];
18
18
  const fetchData = async (method, path, parameterMappings, responsePostProcessor) => {
19
19
  const config = (0, config_1.get)();
20
20
  (0, utils_1.checkConfiguration)(config);
21
- const conf = (0, utils_1.buildHttpConfig)(method, path, parameterMappings);
21
+ const conf = await (0, utils_1.buildHttpConfig)(method, path, parameterMappings);
22
22
  const response = await (0, http_1.fetch)(conf);
23
23
  removeCsrfTokenFromConfig();
24
24
  await (0, utils_1.handleResponse)(response.data, response.headers);
@@ -23,4 +23,4 @@ export declare const handleResponseData: (data: any, outputPath?: string) => Pro
23
23
  export declare function handleResponseHeaders(headers: RawAxiosResponseHeaders | AxiosResponseHeaders): void;
24
24
  export declare const handleResponse: (data: any, headers?: RawAxiosResponseHeaders | AxiosResponseHeaders) => Promise<void>;
25
25
  export declare const configRequiresBody: (method: HTTPMethod) => boolean;
26
- export declare const buildHttpConfig: (method: HTTPMethod, path: string, parameterMappings?: ParameterMappings) => HTTPConfig;
26
+ export declare const buildHttpConfig: (method: HTTPMethod, path: string, parameterMappings?: ParameterMappings) => Promise<HTTPConfig>;
@@ -211,7 +211,7 @@ const handleResponse = async (data, headers) => {
211
211
  exports.handleResponse = handleResponse;
212
212
  const configRequiresBody = (method) => ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
213
213
  exports.configRequiresBody = configRequiresBody;
214
- const buildHttpConfig = (method, path, parameterMappings) => {
214
+ const buildHttpConfig = async (method, path, parameterMappings) => {
215
215
  const config = (0, config_1.get)();
216
216
  const { url, body, headers } = (0, exports.buildParameters)(path, parameterMappings);
217
217
  const httpConfig = {
@@ -31,7 +31,12 @@ const create = () => async (command) => {
31
31
  }
32
32
  logger.debug("reading request body from data");
33
33
  try {
34
- (0, config_1.set)({ data: JSON.parse(input) });
34
+ (0, config_1.set)({
35
+ data: {
36
+ type: "json",
37
+ content: JSON.parse(input),
38
+ },
39
+ });
35
40
  }
36
41
  catch (err) {
37
42
  (0, utils_1.logVerbose)(logger, "Failed to parse input data. Is it a valid escaped JSON string?");
@@ -58,6 +58,7 @@ const loginCommand = {
58
58
  choices: utils_4.getBrowserChoices,
59
59
  default: openUtils_1.getDefaultBrowser,
60
60
  },
61
+ { ...constants_1.OPTION_AUTHORIZATION_FLOW, hidden: false },
61
62
  ]), (0, handler_1.createMandatoryOptionsHandler)(), exports.verifyHost, (0, handler_1.createOauthHandler)(), initializeCache),
62
63
  };
63
64
  exports.default = loginCommand;
package/constants.d.ts CHANGED
@@ -59,3 +59,4 @@ export declare const OPTION_OPTIONS_FILE: Option;
59
59
  export declare const OPTION_FILE_PATH: Option;
60
60
  export declare const OPTION_INPUT: Option;
61
61
  export declare const OPTION_BROWSER: Option;
62
+ export declare const OPTION_AUTHORIZATION_FLOW: Option;
package/constants.js CHANGED
@@ -3,8 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.OPTION_BROWSER = exports.OPTION_INPUT = exports.OPTION_FILE_PATH = exports.OPTION_OPTIONS_FILE = exports.CONFIG_PASSCODE_FUNCTION = exports.OPTION_PASSCODE = exports.OPTION_CODE = exports.OPTION_SECRETS_FILE = exports.OPTION_EXPIRES_IN = exports.OPTION_REFRESH_TOKEN = exports.OPTION_ACCESS_TOKEN = exports.OPTION_TOKEN_URL = exports.OPTION_AUTHORIZATION_URL = exports.OPTION_CLIENT_SECRET = exports.OPTION_CLIENT_ID = exports.OPTION_FORCE = exports.OPTION_VERBOSE = exports.OPTION_NO_PRETTY = exports.OPTION_OUTPUT = exports.OPTION_LOGIN_ID = exports.OPTION_HOST = exports.OPTION_HELP = exports.OPTION_VERSION = exports.CACHE_SECRETS_FILE = exports.PATH_TO_ERROR_HTML = exports.PATH_TO_SUCCESS_HTML = exports.X_OUTPUT_FILE_NAME = exports.X_DSP_API_DEPRECATED_PROPERTIES = exports.X_CSRF_TOKEN = exports.DISCOVERY_METADATA_PATH = exports.SEGMENTS_TO_REMOVE_FOR_PASSCODE_AUTH = exports.CLI_GENERIC_OPTIONS_HELP = exports.CLI_SUPPORTED_AUTHENTICATION_METHODS = exports.CLI_DEPRECATION_MESSAGE = exports.CLI_DEPRECATED = exports.CLI_VERSION = exports.CLI_SAP_HELP = exports.CLI_DISCOVERY_PATHS = exports.CLI_DESCRIPTION = exports.CLI_PACKAGE_NAME = exports.CLI_NAME = exports.ROOT_COMMAND = exports.AuthenticationMethod = exports.DISCOVERY_DOCUMENT_PREFIX = exports.VERSION = void 0;
6
+ exports.OPTION_AUTHORIZATION_FLOW = exports.OPTION_BROWSER = exports.OPTION_INPUT = exports.OPTION_FILE_PATH = exports.OPTION_OPTIONS_FILE = exports.CONFIG_PASSCODE_FUNCTION = exports.OPTION_PASSCODE = exports.OPTION_CODE = exports.OPTION_SECRETS_FILE = exports.OPTION_EXPIRES_IN = exports.OPTION_REFRESH_TOKEN = exports.OPTION_ACCESS_TOKEN = exports.OPTION_TOKEN_URL = exports.OPTION_AUTHORIZATION_URL = exports.OPTION_CLIENT_SECRET = exports.OPTION_CLIENT_ID = exports.OPTION_FORCE = exports.OPTION_VERBOSE = exports.OPTION_NO_PRETTY = exports.OPTION_OUTPUT = exports.OPTION_LOGIN_ID = exports.OPTION_HOST = exports.OPTION_HELP = exports.OPTION_VERSION = exports.CACHE_SECRETS_FILE = exports.PATH_TO_ERROR_HTML = exports.PATH_TO_SUCCESS_HTML = exports.X_OUTPUT_FILE_NAME = exports.X_DSP_API_DEPRECATED_PROPERTIES = exports.X_CSRF_TOKEN = exports.DISCOVERY_METADATA_PATH = exports.SEGMENTS_TO_REMOVE_FOR_PASSCODE_AUTH = exports.CLI_GENERIC_OPTIONS_HELP = exports.CLI_SUPPORTED_AUTHENTICATION_METHODS = exports.CLI_DEPRECATION_MESSAGE = exports.CLI_DEPRECATED = exports.CLI_VERSION = exports.CLI_SAP_HELP = exports.CLI_DISCOVERY_PATHS = exports.CLI_DESCRIPTION = exports.CLI_PACKAGE_NAME = exports.CLI_NAME = exports.ROOT_COMMAND = exports.AuthenticationMethod = exports.DISCOVERY_DOCUMENT_PREFIX = exports.VERSION = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
+ const types_1 = require("./types");
8
9
  const utils_1 = require("./utils/utils");
9
10
  exports.VERSION = (0, utils_1.getVersion)();
10
11
  exports.DISCOVERY_DOCUMENT_PREFIX = "discovery-";
@@ -214,3 +215,15 @@ exports.OPTION_BROWSER = {
214
215
  type: "select",
215
216
  },
216
217
  };
218
+ exports.OPTION_AUTHORIZATION_FLOW = {
219
+ longName: "authorization-flow",
220
+ description: "specifies the authorization flow to use",
221
+ args: [{ name: "authorization-flow" }],
222
+ choices: [
223
+ types_1.GrantType.authorization_code,
224
+ types_1.GrantType.client_credentials,
225
+ ],
226
+ default: types_1.GrantType.authorization_code,
227
+ required: true,
228
+ hidden: true,
229
+ };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@sap/cli-core",
3
- "version": "2024.22.0",
3
+ "version": "2024.24.0",
4
4
  "description": "Command-Line Interface (CLI) Core Module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "SAP SE",
7
7
  "homepage": "https://www.sap.com",
8
8
  "main": "index.js",
9
9
  "engines": {
10
- "node": "^18 || ^19 || ^20 || ^21",
10
+ "node": "^18 || ^19 || ^20 || ^21 || ^22",
11
11
  "npm": "^9 || ^10"
12
12
  },
13
13
  "keywords": [
@@ -23,7 +23,7 @@
23
23
  "compare-versions": "6.1.1",
24
24
  "config": "3.3.12",
25
25
  "dotenv": "16.4.5",
26
- "form-data": "4.0.0",
26
+ "form-data": "4.0.1",
27
27
  "fs-extra": "11.2.0",
28
28
  "https": "1.0.0",
29
29
  "https-proxy-agent": "7.0.5",
package/types.d.ts CHANGED
@@ -16,6 +16,7 @@ export type Secret = {
16
16
  jti: string;
17
17
  scope: string;
18
18
  id_token: string;
19
+ authorization_flow: GrantType;
19
20
  };
20
21
  export declare const ALLOWED_SECRET_TYPES: string[];
21
22
  export { HTTPMethod, HTTPConfig, HTTPResponse };
@@ -211,3 +212,8 @@ export type DeprecatedProperties = Array<{
211
212
  export type Command = TopCommand | LeafCommand;
212
213
  export type AddCommands = (program: CommandType) => Promise<void>;
213
214
  export type ResponsePostProcessor = (response: HTTPResponse) => Promise<void>;
215
+ export declare enum GrantType {
216
+ authorization_code = "authorization_code",
217
+ client_credentials = "client_credentials",
218
+ refresh_token = "refresh_token"
219
+ }
package/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CHARACTERS = exports.LogLevel = exports.ALLOWED_SECRET_TYPES = void 0;
3
+ exports.GrantType = exports.CHARACTERS = exports.LogLevel = exports.ALLOWED_SECRET_TYPES = void 0;
4
4
  // unfortunately there does not seem to be a good solution to automatically
5
5
  // create the below array from a statically defined type. Thus, the array must
6
6
  // always be kept in sync with the type Secret defined above.
@@ -20,6 +20,7 @@ exports.ALLOWED_SECRET_TYPES = [
20
20
  "jti",
21
21
  "scope",
22
22
  "id_token",
23
+ "authorization_flow",
23
24
  ];
24
25
  var LogLevel;
25
26
  (function (LogLevel) {
@@ -85,3 +86,9 @@ exports.CHARACTERS = [
85
86
  "Y",
86
87
  "Z",
87
88
  ];
89
+ var GrantType;
90
+ (function (GrantType) {
91
+ GrantType["authorization_code"] = "authorization_code";
92
+ GrantType["client_credentials"] = "client_credentials";
93
+ GrantType["refresh_token"] = "refresh_token";
94
+ })(GrantType || (exports.GrantType = GrantType = {}));
package/utils/utils.d.ts CHANGED
@@ -15,6 +15,7 @@ export declare const getEngines: (cwd?: string) => PackageJson["engines"];
15
15
  export declare const getBin: (cwd?: string) => string;
16
16
  export declare const removeScopeFromPackageName: (packageName: string) => string;
17
17
  export declare const parseTenant: (tenant: string) => string;
18
+ export declare function isValidURL(url: string): boolean;
18
19
  export declare const getInfoFromTenant: (tenant: string, verbose: boolean, printOutput?: boolean) => {
19
20
  host: string;
20
21
  publicfqdn: string;
package/utils/utils.js CHANGED
@@ -28,6 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.toConstantCase = exports.sha256 = exports.getInfoFromTenant = exports.parseTenant = exports.removeScopeFromPackageName = exports.getBin = exports.getEngines = exports.getDescription = exports.getPackageName = exports.getName = exports.getVersion = exports.readPackageJson = exports.requireFile = exports.parseVersion = void 0;
30
30
  exports.buildOptionName = buildOptionName;
31
+ exports.isValidURL = isValidURL;
31
32
  exports.removeQueryParametersFromUrl = removeQueryParametersFromUrl;
32
33
  const crypto = __importStar(require("crypto"));
33
34
  const path_1 = __importDefault(require("path"));
@@ -124,6 +125,19 @@ const parseTenant = (tenant) => {
124
125
  return new URL(t).host;
125
126
  };
126
127
  exports.parseTenant = parseTenant;
128
+ function isValidURL(url) {
129
+ const { debug, error } = getLogger();
130
+ try {
131
+ // eslint-disable-next-line no-new
132
+ new URL(url);
133
+ debug(`url ${url} is valid`);
134
+ return true;
135
+ }
136
+ catch (err) {
137
+ error(`url ${url} is no valid url`, err);
138
+ return false;
139
+ }
140
+ }
127
141
  const getInfoFromTenant = (tenant, verbose, printOutput = true) => {
128
142
  let protocol;
129
143
  try {