@sap/cli-core 2025.12.0 → 2025.14.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,21 @@ 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
+ ## 2025.13.0
9
+
10
+ ### Changed
11
+
12
+ - The login experience has been improved: When logging in, it is now clearly indicated if a secret for a tenant already exists, and confirmation is requested before overwriting it. Previously, existing secrets were not overwritten, but this was not clearly communicated. The process is now transparent and user-driven, helping to prevent accidental loss of credentials and confusion.
13
+
14
+ Example:
15
+
16
+ ```bash
17
+ $ <cli> login
18
+ ? Secret for tenant https://example-tenant.hanacloudservices.cloud.sap already exists. Do you want to overwrite it? › (Y/n)
19
+ ```
20
+
21
+ After confirming, the CLI will overwrite the existing secret with the new one. If the user chooses not to overwrite, the CLI will exit gracefully without making any changes.
22
+
8
23
  ## 2025.11.0
9
24
 
10
25
  ### Fixed
@@ -21,4 +21,5 @@ export declare class SecretsStorageImpl implements SecretsStorage {
21
21
  private removeUnknownPropertiesFromSecrets;
22
22
  synchronizeSecretsToStorage(): Promise<void>;
23
23
  getAllSecrets(): ReadonlyArray<Secret>;
24
+ hasSecretForTenant(tenantUrl: string): boolean;
24
25
  }
@@ -77,7 +77,13 @@ class SecretsStorageImpl {
77
77
  let prevSecretIx = this.secrets.findIndex((s) => s.tenantUrl === tenantUrl);
78
78
  let newSecret;
79
79
  if (prevSecretIx > -1) {
80
- newSecret = { ...this.secrets[prevSecretIx], ...secret };
80
+ // Overwrite the old secret completely except for id and tenantUrl
81
+ newSecret = {
82
+ ...secret,
83
+ id: this.secrets[prevSecretIx].id,
84
+ tenantUrl,
85
+ customClient: false,
86
+ };
81
87
  this.secrets[prevSecretIx] = newSecret;
82
88
  }
83
89
  else {
@@ -136,5 +142,8 @@ class SecretsStorageImpl {
136
142
  getAllSecrets() {
137
143
  return this.secrets;
138
144
  }
145
+ hasSecretForTenant(tenantUrl) {
146
+ return this.secrets.some((secret) => secret.tenantUrl === tenantUrl);
147
+ }
139
148
  }
140
149
  exports.SecretsStorageImpl = SecretsStorageImpl;
@@ -1,2 +1,2 @@
1
1
  import { Handler } from "../../../../types";
2
- export declare const create: () => Handler;
2
+ export declare const create: (overrideExisting?: boolean) => Handler;
@@ -9,9 +9,9 @@ const tokenProvider_1 = require("./tokenProvider");
9
9
  const checkOptionsExistence_1 = require("../../checkOptionsExistence");
10
10
  const constants_1 = require("../../../../constants");
11
11
  const core_1 = require("../../../../config/core");
12
- const create = () => {
12
+ const create = (overrideExisting = false) => {
13
13
  if ((0, core_1.getAuthenticationMethods)().includes(constants_1.AuthenticationMethod.oauth)) {
14
- return (0, error_1.create)("failed to handle OAuth authorization", (0, next_1.create)("commands.handler.authentication.oauth", (0, checkOptionsExistence_1.create)(constants_1.OPTION_PASSCODE), (0, secretsProvider_1.create)(), (0, tokenProvider_1.create)()));
14
+ return (0, error_1.create)("failed to handle OAuth authorization", (0, next_1.create)("commands.handler.authentication.oauth", (0, checkOptionsExistence_1.create)(constants_1.OPTION_PASSCODE), (0, secretsProvider_1.create)(), (0, tokenProvider_1.create)(overrideExisting)));
15
15
  }
16
16
  return (0, fail_1.create)();
17
17
  };
@@ -1,2 +1,2 @@
1
1
  import { Handler } from "../../../../../types";
2
- export declare const create: () => Handler;
2
+ export declare const create: (overrideExisting?: boolean) => Handler;
@@ -10,30 +10,33 @@ const utils_1 = require("../utils");
10
10
  const utils_2 = require("./utils");
11
11
  const SecretsStorageSingleton_1 = require("../../../../../cache/secrets/SecretsStorageSingleton");
12
12
  const getLogger = () => (0, logger_1.get)("commands.handler.authentication.oauth.tokenProvider.getToken");
13
- const handler = async () => async () => {
14
- const { info: logInfo, debug } = getLogger();
15
- logInfo("checking token existence");
16
- const secrets = await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.getDefaultSecret();
17
- if (!secrets.access_token && !secrets.refresh_token) {
18
- debug("access token not available, retrieving token from server");
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 });
13
+ const createGetTokenHandler = (overrideExisting) => {
14
+ const handler = async () => async () => {
15
+ const { info: logInfo, debug } = getLogger();
16
+ logInfo("checking token existence");
17
+ const secrets = await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.getDefaultSecret();
18
+ if (overrideExisting || (!secrets.access_token && !secrets.refresh_token)) {
19
+ debug(`access token not available or overrideExisting=${overrideExisting}, retrieving token from server`);
20
+ if (secrets.authorization_flow === types_1.GrantType.authorization_code) {
21
+ const code = await (0, utils_2.getCode)(secrets.authorization_url, secrets.client_id);
22
+ debug("code received, reading token");
23
+ await (0, utils_1.readToken)({ code, grant_type: secrets.authorization_flow });
24
+ }
25
+ else if (secrets.authorization_flow === types_1.GrantType.client_credentials) {
26
+ await (0, utils_1.readToken)({ grant_type: secrets.authorization_flow });
27
+ }
28
+ else {
29
+ throw new Error(`invalid grant type ${secrets.authorization_flow}`);
30
+ }
23
31
  }
24
- else if (secrets.authorization_flow === types_1.GrantType.client_credentials) {
25
- await (0, utils_1.readToken)({ grant_type: secrets.authorization_flow });
32
+ else if (secrets.access_token) {
33
+ debug("token available");
26
34
  }
27
35
  else {
28
- throw new Error(`invalid grant type ${secrets.authorization_flow}`);
36
+ throw new Error("access token not available");
29
37
  }
30
- }
31
- else if (secrets.access_token) {
32
- debug("token available");
33
- }
34
- else {
35
- throw new Error("access token not available");
36
- }
38
+ };
39
+ return handler;
37
40
  };
38
- const create = () => (0, next_1.create)("commands.handler.authentication.oauth.tokenProvider.getToken", (0, options_1.create)(constants_1.OPTION_CODE), handler);
41
+ const create = (overrideExisting = false) => (0, next_1.create)("commands.handler.authentication.oauth.tokenProvider.getToken", (0, options_1.create)(constants_1.OPTION_CODE), createGetTokenHandler(overrideExisting));
39
42
  exports.create = create;
@@ -1,2 +1,2 @@
1
1
  import { Handler } from "../../../../../types";
2
- export declare const create: () => Handler;
2
+ export declare const create: (overrideExisting?: boolean) => Handler;
@@ -7,5 +7,5 @@ const or_1 = require("../../../or");
7
7
  const refreshToken_1 = require("./refreshToken");
8
8
  const setAuthorization_1 = require("./setAuthorization");
9
9
  const getToken_1 = require("./getToken");
10
- const create = () => (0, error_1.create)("failed to handle token", (0, next_1.create)("commands.handler.authentication.oauth.tokenProvider", (0, or_1.create)("commands.handler.authentication.oauth.tokenProvider", (0, getToken_1.create)(), (0, refreshToken_1.create)(true)), (0, setAuthorization_1.create)()));
10
+ const create = (overrideExisting = false) => (0, error_1.create)("failed to handle token", (0, next_1.create)("commands.handler.authentication.oauth.tokenProvider", (0, or_1.create)("commands.handler.authentication.oauth.tokenProvider", (0, getToken_1.create)(overrideExisting), (0, refreshToken_1.create)(true)), (0, setAuthorization_1.create)()));
11
11
  exports.create = create;
@@ -16,8 +16,8 @@ const isExpired = (expires_after) => expires_after <= (0, exports.calculateExpir
16
16
  exports.isExpired = isExpired;
17
17
  const readToken = async (data) => {
18
18
  const logger = getLogger();
19
- const secrets = await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.getDefaultSecret();
20
- if (!secrets.token_url || !secrets.client_id || !secrets.client_secret) {
19
+ const secret = await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.getDefaultSecret();
20
+ if (!secret.token_url || !secret.client_id || !secret.client_secret) {
21
21
  (0, utils_1.logVerbose)(logger, `The secret is invalid. Either the token URL, client ID, or client secret is missing.` +
22
22
  `Please check the secret and login again.`);
23
23
  throw new Error("invalid secrets information");
@@ -26,26 +26,26 @@ const readToken = async (data) => {
26
26
  ...data,
27
27
  response_type: "token",
28
28
  };
29
- if (!secrets.customClient) {
30
- body.client_id = secrets.client_id;
29
+ if (!secret.customClient) {
30
+ body.client_id = secret.client_id;
31
31
  }
32
32
  logger.debug(`reading token`);
33
33
  const info = (await (0, http_1.fetch)({
34
34
  method: "POST",
35
- url: secrets.token_url,
35
+ url: secret.token_url,
36
36
  auth: {
37
- username: secrets.client_id,
38
- password: secrets.client_secret,
37
+ username: secret.client_id,
38
+ password: secret.client_secret,
39
39
  },
40
40
  headers: {
41
- "x-sap-sac-custom-auth": !!secrets.customClient,
41
+ "x-sap-sac-custom-auth": !!secret.customClient,
42
42
  "content-type": "application/x-www-form-urlencoded",
43
43
  },
44
44
  data: qs_1.default.stringify(body),
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
+ ...secret,
49
49
  ...info,
50
50
  expires_after: (0, exports.calculateExpiresAfter)(info.expires_in),
51
51
  });
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { Option } from "../../../types";
3
3
  export declare const checkOptions: (options: Array<Option> | Option, command: Command) => Promise<void>;
4
- export declare const promptForValue: (option: Option) => Promise<string>;
4
+ export declare const promptForValue: <ResponseType = string>(option: Option) => Promise<ResponseType>;
5
5
  export declare const setOption: (option: Option, value: string) => void;
6
6
  export declare const getValueFromOptionsFile: (filePath: string, { longName }: Option) => Promise<string>;
@@ -11,6 +11,7 @@ const options_1 = require("../utils/options");
11
11
  const utils_3 = require("../utils/utils");
12
12
  const init_command_1 = require("./config.command/cache.command/init.command");
13
13
  const handler_1 = require("./handler");
14
+ const utils_4 = require("./handler/options/utils");
14
15
  const getLogger = () => (0, logger_1.get)("commands.login");
15
16
  const verifyHost = async () => async () => {
16
17
  const logger = getLogger();
@@ -25,6 +26,18 @@ const verifyHost = async () => async () => {
25
26
  }
26
27
  };
27
28
  exports.verifyHost = verifyHost;
29
+ const confirmSecretsOverwrite = async (tenantUrl) => {
30
+ const promptResponse = await (0, utils_4.promptForValue)({
31
+ prompts: {
32
+ type: "confirm",
33
+ message: `Secret for tenant ${tenantUrl} already exists. Do you want to overwrite it?`,
34
+ initial: true,
35
+ },
36
+ longName: "overwrite-secrets",
37
+ description: `overwrite existing secrets`,
38
+ });
39
+ return promptResponse;
40
+ };
28
41
  const initializeCache = async () => async () => {
29
42
  const { warn, info } = getLogger();
30
43
  info("initializing cache after successful login");
@@ -43,6 +56,25 @@ const fetchSupportedBrowsers = async () => {
43
56
  const saveSecrets = async () => async () => {
44
57
  await SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON.synchronizeSecretsToStorage();
45
58
  };
59
+ const warnIfSecretExists = async () => {
60
+ return async () => {
61
+ const logger = getLogger();
62
+ const tenantUrl = (0, utils_1.getTenantUrl)();
63
+ const secretsStorage = SecretsStorageSingleton_1.SecretsStorageSingleton.SINGLETON;
64
+ if (secretsStorage.hasSecretForTenant &&
65
+ secretsStorage.hasSecretForTenant(tenantUrl)) {
66
+ const shouldOverwrite = await confirmSecretsOverwrite(tenantUrl);
67
+ logger.debug(`user response for overwriting secrets for tenant ${tenantUrl}: ${shouldOverwrite}`);
68
+ if (!shouldOverwrite) {
69
+ const message = `Aborted: Secret for tenant ${tenantUrl} already exists and will not be overwritten.`;
70
+ logger.warn(message);
71
+ // Do not throw, just log and return
72
+ return;
73
+ }
74
+ }
75
+ throw new Error(`Warning: Previous login secrets for tenant ${tenantUrl} will be removed and overwritten with new credentials.`);
76
+ };
77
+ };
46
78
  const loginCommand = {
47
79
  type: "command",
48
80
  command: "login",
@@ -66,6 +98,6 @@ const loginCommand = {
66
98
  default: openUtils_1.getDefaultBrowser,
67
99
  },
68
100
  { ...constants_1.OPTION_AUTHORIZATION_FLOW, hidden: false },
69
- ]), (0, handler_1.createMandatoryOptionsHandler)(), exports.verifyHost, (0, handler_1.createOauthHandler)(), initializeCache, saveSecrets),
101
+ ]), (0, handler_1.createMandatoryOptionsHandler)(), exports.verifyHost, (0, handler_1.createOrHandler)("commands.login", warnIfSecretExists, (0, handler_1.createNextHandler)("commands.login", (0, handler_1.createOauthHandler)(true), initializeCache, saveSecrets))),
70
102
  };
71
103
  exports.default = loginCommand;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cli-core",
3
- "version": "2025.12.0",
3
+ "version": "2025.14.0",
4
4
  "description": "Command-Line Interface (CLI) Core Module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "SAP SE",
@@ -21,9 +21,9 @@
21
21
  "axios": "1.9.0",
22
22
  "commander": "12.1.0",
23
23
  "compare-versions": "6.1.1",
24
- "config": "3.3.12",
24
+ "config": "4.0.0",
25
25
  "dotenv": "16.5.0",
26
- "form-data": "4.0.2",
26
+ "form-data": "4.0.3",
27
27
  "fs-extra": "11.3.0",
28
28
  "https": "1.0.0",
29
29
  "https-proxy-agent": "7.0.6",
@@ -33,7 +33,7 @@ const openUrlInBrowser = async (url, queryParameters = {}) => {
33
33
  }
34
34
  const urlString = u.toString();
35
35
  const { debug, error } = getLogger();
36
- let browser = await getBrowser();
36
+ const browser = await getBrowser();
37
37
  const open = (await import("open")).default;
38
38
  try {
39
39
  debug(`Attempting to open browser ${browser} at ${urlString}`);