@sap/cli-core 2025.11.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 +15 -0
- package/cache/secrets/SecretsStorageImpl.d.ts +1 -0
- package/cache/secrets/SecretsStorageImpl.js +10 -1
- package/commands/handler/authentication/oauth/index.d.ts +1 -1
- package/commands/handler/authentication/oauth/index.js +2 -2
- package/commands/handler/authentication/oauth/tokenProvider/getToken.d.ts +1 -1
- package/commands/handler/authentication/oauth/tokenProvider/getToken.js +24 -21
- package/commands/handler/authentication/oauth/tokenProvider/index.d.ts +1 -1
- package/commands/handler/authentication/oauth/tokenProvider/index.js +1 -1
- package/commands/handler/authentication/oauth/utils.js +9 -9
- package/commands/handler/options/utils.d.ts +1 -1
- package/commands/login.command.js +33 -1
- package/package.json +4 -4
- package/utils/openUtils.js +1 -1
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
|
|
@@ -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
|
-
|
|
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
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
25
|
-
|
|
32
|
+
else if (secrets.access_token) {
|
|
33
|
+
debug("token available");
|
|
26
34
|
}
|
|
27
35
|
else {
|
|
28
|
-
throw new Error(
|
|
36
|
+
throw new Error("access token not available");
|
|
29
37
|
}
|
|
30
|
-
}
|
|
31
|
-
|
|
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),
|
|
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
|
|
20
|
-
if (!
|
|
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 (!
|
|
30
|
-
body.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:
|
|
35
|
+
url: secret.token_url,
|
|
36
36
|
auth: {
|
|
37
|
-
username:
|
|
38
|
-
password:
|
|
37
|
+
username: secret.client_id,
|
|
38
|
+
password: secret.client_secret,
|
|
39
39
|
},
|
|
40
40
|
headers: {
|
|
41
|
-
"x-sap-sac-custom-auth": !!
|
|
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
|
-
...
|
|
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<
|
|
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.
|
|
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,14 +21,14 @@
|
|
|
21
21
|
"axios": "1.9.0",
|
|
22
22
|
"commander": "12.1.0",
|
|
23
23
|
"compare-versions": "6.1.1",
|
|
24
|
-
"config": "
|
|
24
|
+
"config": "4.0.0",
|
|
25
25
|
"dotenv": "16.5.0",
|
|
26
|
-
"form-data": "4.0.
|
|
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",
|
|
30
30
|
"lodash": "4.17.21",
|
|
31
|
-
"open": "10.1.
|
|
31
|
+
"open": "10.1.2",
|
|
32
32
|
"path": "0.12.7",
|
|
33
33
|
"prompts": "2.4.2",
|
|
34
34
|
"qs": "6.14.0"
|
package/utils/openUtils.js
CHANGED
|
@@ -33,7 +33,7 @@ const openUrlInBrowser = async (url, queryParameters = {}) => {
|
|
|
33
33
|
}
|
|
34
34
|
const urlString = u.toString();
|
|
35
35
|
const { debug, error } = getLogger();
|
|
36
|
-
|
|
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}`);
|