Skip to content

Commit 9f0a500

Browse files
Merge pull request #2067 from contentstack/staging
DX | 18-08-2025 | Release
2 parents 2f05e4c + 1338654 commit 9f0a500

File tree

27 files changed

+3225
-2784
lines changed

27 files changed

+3225
-2784
lines changed

.talismanrc

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
fileignoreconfig:
22
- filename: package-lock.json
3-
checksum: c73d080bfbbf03bc2597891f956367bfd71f5f2f8d03525175426015265edc91
3+
checksum: b01e61e606c0f2fd26cee8c27e00f5910973020afa72ddfb9d995f09dff6229f
44
- filename: pnpm-lock.yaml
5-
checksum: 0ca85066946c49994a4353c9f64b8f380d5d2050194e3e57ad7ccd7faa030d36
5+
checksum: e0310a5c5772a87bf606ee80722e29f9d0bc412ce10444f825a5c14dce4c1b88
66
- filename: packages/contentstack-import-setup/test/unit/backup-handler.test.ts
77
checksum: 0582d62b88834554cf12951c8690a73ef3ddbb78b82d2804d994cf4148e1ef93
88
- filename: packages/contentstack-import-setup/test/config.json
@@ -32,7 +32,29 @@ fileignoreconfig:
3232
- filename: packages/contentstack-import-setup/test/unit/login-handler.test.ts
3333
checksum: e549f9ca3a9aae0d93b7284f7e771d55c0610725ddcb4333612df2f215e92769
3434
- filename: packages/contentstack/README.md
35-
checksum: c09f6dc93702caff3adf689b501ec32586a16c865c1fe3a63b53ae645ca22349
35+
checksum: f46084b199b3b0d7986b363c86a657570def71e5da29b948cc343eaf94ec7e97
3636
- filename: packages/contentstack-import-setup/test/unit/modules/assets.test.ts
3737
checksum: 449a5e3383631a6f78d1291aa3c28c91681879289398f0a933158fba5c5d5acf
38-
version: "1.0"
38+
- filename: packages/contentstack-auth/env.example
39+
checksum: 72c9ed18a449c42b03ec54795898f6bad4e15d23a3d701c05b96fb17c3bbd93b
40+
- filename: packages/contentstack-auth/test/integration/auth.test.ts
41+
checksum: 9933a64d17d6d6dd7dd87ff210ce5e8a215bf36fac0cfd333894612ed10fb81b
42+
- filename: packages/contentstack-auth/src/utils/mfa-handler.ts
43+
checksum: ca9c34a3fe6c3b957debff987aefbceb641bf4954f15541d07d901f91e5ff014
44+
- filename: packages/contentstack-auth/messages/index.json
45+
checksum: 95856ad6273f17a9e853cda9c2cf0bdd782e47aeab93385e73ab870b5e814f89
46+
- filename: packages/contentstack-auth/test/utils/auth-handler.test.ts
47+
checksum: f88dded3a326f191844e39258e7fe390a72fefeb387d09c7f97e4e8aed520c97
48+
- filename: packages/contentstack-auth/src/commands/auth/login.ts
49+
checksum: 89204be8dfc1f670a568af992b54f34845e49bd4a8046c0cf041dd3759150718
50+
- filename: packages/contentstack-auth/test/unit/commands/tokens-add.test.ts
51+
checksum: 1e7247908e1887998210381c03caca93a3983e1c8967483464cf1c3bd3209cd1
52+
- filename: packages/contentstack-auth/test/unit/commands/logout.test.ts
53+
checksum: cd22dd04bd6a77cafa7dd0960cd4691201a3e228216d5a10041b8e39d7ebba1f
54+
- filename: packages/contentstack-auth/src/utils/auth-handler.ts
55+
checksum: 1261d02e8215da2db28557b77d6a8c8c604e11df88520e1cc5c8561e26bdd150
56+
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
57+
checksum: f93aa9b0c964608b60c88d4c72ff33840b58ec900297c4bae1f4ea365aa51048
58+
- filename: packages/contentstack-auth/test/utils/mfa-handler.test.ts
59+
checksum: b067f93cf0185d794e8419cc41e8fac96ed790dea8fc48dc083ee242ccacbd4d
60+
version: "1.0"

package-lock.json

Lines changed: 393 additions & 387 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/contentstack-auth/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ $ npm install -g @contentstack/cli-auth
1818
$ csdx COMMAND
1919
running command...
2020
$ csdx (--version)
21-
@contentstack/cli-auth/1.5.1 darwin-arm64 node-v22.13.1
21+
@contentstack/cli-auth/1.6.0 darwin-arm64 node-v22.14.0
2222
$ csdx --help [COMMAND]
2323
USAGE
2424
$ csdx COMMAND

packages/contentstack-auth/env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ BRANCH_ENABLED_DELIVERY_TOKEN
66
BRANCH_DISABLED_DELIVERY_TOKEN
77
BRANCH_ENABLED_ENVIRONMENT
88
BRANCH_DISABLED_ENVIRONMENT
9+
CONTENTSTACK_MFA_SECRET

packages/contentstack-auth/messages/index.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,16 @@
4949
"CLI_AUTH_TOKENS_VALIDATION_INVALID_API_KEY": "Invalid api key",
5050
"CLI_AUTH_EXIT_PROCESS": "Exiting the process...",
5151
"CLI_SELECT_TOKEN_TYPE": "Select the type of token to add",
52-
"CLI_AUTH_ENTER_BRANCH": "Enter branch name"
52+
"CLI_AUTH_MFA_INVALID_SECRET": "Invalid MFA secret format. Verify your authentication setup.",
53+
"CLI_AUTH_MFA_GENERATION_FAILED": "Failed to generate MFA code. Switching to manual MFA code entry.",
54+
"CLI_AUTH_MFA_DECRYPT_FAILED": "Failed to decrypt stored MFA secret. Try Resetting the MFA secret. Proceeding for Manual MFA code input.",
55+
"CLI_AUTH_MFA_INVALID_CODE": "Invalid authentication code format.",
56+
"CLI_AUTH_MFA_RECONFIGURE_HINT": "Consider reconfiguring MFA using config:mfa:add",
57+
"CLI_AUTH_SMS_OTP_FAILED": "Failed to send SMS OTP. Try again or use a different two-factor authentication method.",
58+
"CLI_AUTH_2FA_FAILED": "Two-factor authentication failed! Try again.",
59+
"CLI_AUTH_LOGIN_NO_USER": "No user found with the provided credentials!",
60+
"CLI_AUTH_LOGIN_NO_CREDENTIALS": "No credentials provided. Enter your email and password to log in.",
61+
"CLI_AUTH_LOGOUT_NO_TOKEN": "No auth token found. Log in before logging out.",
62+
"CLI_AUTH_TOKEN_VALIDATION_FAILED": "Token validation failed. Log in again.",
63+
"CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN": "No auth token found. Log in to continue.."
5364
}

packages/contentstack-auth/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@contentstack/cli-auth",
33
"description": "Contentstack CLI plugin for authentication activities",
4-
"version": "1.5.1",
4+
"version": "1.6.0",
55
"author": "Contentstack",
66
"bugs": "https://github.com/contentstack/cli/issues",
77
"scripts": {
@@ -25,7 +25,8 @@
2525
"@contentstack/cli-command": "~1.6.0",
2626
"@contentstack/cli-utilities": "~1.13.1",
2727
"@oclif/core": "^4.3.0",
28-
"@oclif/plugin-help": "^6.2.28"
28+
"@oclif/plugin-help": "^6.2.28",
29+
"otplib": "^12.0.1"
2930
},
3031
"devDependencies": {
3132
"@fancy-test/nock": "^0.1.1",

packages/contentstack-auth/src/commands/auth/login.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
messageHandler,
1111
} from '@contentstack/cli-utilities';
1212
import { User } from '../../interfaces';
13-
import { authHandler, interactive } from '../../utils';
13+
import { authHandler, interactive, mfaHandler } from '../../utils';
1414
import { BaseCommand } from '../../base-command';
1515

1616
export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
@@ -40,6 +40,7 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
4040
required: false,
4141
exclusive: ['oauth'],
4242
}),
43+
4344
oauth: flags.boolean({
4445
description: 'Enables single sign-on (SSO) in Contentstack CLI.',
4546
required: false,
@@ -57,9 +58,9 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
5758
log.debug('Initializing management API client', this.contextDetails);
5859
const managementAPIClient = await managementSDKClient({ host: this.cmaHost, skipTokenValidity: true });
5960
log.debug('Management API client initialized successfully', this.contextDetails);
60-
61+
6162
const { flags: loginFlags } = await this.parse(LoginCommand);
62-
log.debug('Token add flags parsed', {...this.contextDetails, flags: loginFlags});
63+
log.debug('Token add flags parsed', { ...this.contextDetails, flags: loginFlags });
6364

6465
authHandler.client = managementAPIClient;
6566
log.debug('Auth handler client set', this.contextDetails);
@@ -76,12 +77,22 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
7677
log.debug('Starting basic authentication flow', this.contextDetails);
7778
const username = loginFlags?.username || (await interactive.askUsername());
7879
const password = loginFlags?.password || (await interactive.askPassword());
79-
log.debug('Credentials obtained', { ...this.contextDetails, hasUsername: !!username, hasPassword: !!password });
80+
log.debug('Credentials obtained', {
81+
...this.contextDetails,
82+
hasUsername: !!username,
83+
hasPassword: !!password,
84+
});
85+
8086
await this.login(username, password);
8187
}
8288
} catch (error) {
83-
log.debug('Login command failed', { ...this.contextDetails, error });
84-
cliux.error('CLI_AUTH_LOGIN_FAILED');
89+
log.debug('Login command failed', {
90+
...this.contextDetails,
91+
error,
92+
});
93+
if ((error?.message && error?.message.includes('2FA')) || error?.message.includes('MFA')) {
94+
error.message = `${error.message}\nFor more information about MFA, visit: https://www.contentstack.com/docs/developers/security/multi-factor-authentication`;
95+
}
8596
handleAndLogError(error, { ...this.contextDetails });
8697
process.exit();
8798
}
@@ -92,7 +103,19 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
92103

93104
try {
94105
log.debug('Calling auth handler login', this.contextDetails);
95-
const user: User = await authHandler.login(username, password);
106+
let tfaToken: string | undefined;
107+
108+
try {
109+
tfaToken = await mfaHandler.getMFACode();
110+
if (tfaToken) {
111+
log.debug('MFA token generated from stored configuration', this.contextDetails);
112+
}
113+
} catch (error) {
114+
log.debug('Failed to generate MFA token from config', { ...this.contextDetails, error });
115+
tfaToken = undefined;
116+
}
117+
118+
const user: User = await authHandler.login(username, password, tfaToken);
96119
log.debug('Auth handler login completed', {
97120
...this.contextDetails,
98121
hasUser: !!user,

packages/contentstack-auth/src/utils/auth-handler.ts

Lines changed: 71 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cliux, CLIError, log, cliErrorHandler } from '@contentstack/cli-utilities';
1+
import { cliux, log, handleAndLogError, messageHandler } from '@contentstack/cli-utilities';
22
import { User } from '../interfaces';
33
import { askOTPChannel, askOTP } from './interactive';
44

@@ -27,9 +27,61 @@ class AuthHandler {
2727
* @returns {Promise} Promise object returns authtoken on success
2828
* TBD: take out the otp implementation from login and create a new method/function to handle otp
2929
*/
30+
/**
31+
* Handle the OTP flow for 2FA authentication
32+
* @param tfaToken Optional pre-provided TFA token
33+
* @param loginPayload Login payload containing user credentials
34+
* @returns Promise<string> The TFA token to use for authentication
35+
*/
36+
private async handleOTPFlow(tfaToken?: string, loginPayload?: any): Promise<string> {
37+
try {
38+
if (tfaToken) {
39+
log.info('Using provided TFA token', { module: 'auth-handler' });
40+
return tfaToken;
41+
}
42+
43+
log.debug('2FA required, requesting OTP channel', { module: 'auth-handler' });
44+
const otpChannel = await askOTPChannel();
45+
log.debug(`OTP channel selected: ${otpChannel}`, { module: 'auth-handler' });
46+
47+
if (otpChannel === 'sms') {
48+
try {
49+
await this.requestSMSOTP(loginPayload);
50+
} catch (error) {
51+
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
52+
cliux.print('CLI_AUTH_SMS_OTP_FAILED', { color: 'red' });
53+
throw error;
54+
}
55+
}
56+
57+
log.debug('Requesting OTP input', { module: 'auth-handler', channel: otpChannel });
58+
return await askOTP();
59+
} catch (error) {
60+
log.debug('2FA flow failed', { module: 'auth-handler', error });
61+
throw error;
62+
}
63+
}
64+
65+
/**
66+
* Request SMS OTP for 2FA authentication
67+
* @param loginPayload Login payload containing user credentials
68+
* @throws CLIError if SMS request fails
69+
*/
70+
private async requestSMSOTP(loginPayload: any): Promise<void> {
71+
log.debug('Sending SMS OTP request', { module: 'auth-handler' });
72+
try {
73+
await this._client.axiosInstance.post('/user/request_token_sms', { user: loginPayload });
74+
log.debug('SMS OTP request successful', { module: 'auth-handler' });
75+
cliux.print('CLI_AUTH_LOGIN_SECURITY_CODE_SEND_SUCCESS');
76+
} catch (error) {
77+
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
78+
throw error;
79+
}
80+
}
81+
3082
async login(email: string, password: string, tfaToken?: string): Promise<User> {
31-
const hasCredentials = !!password;
32-
const hasTfaToken = !!tfaToken;
83+
const hasCredentials = typeof password === 'string' && password.length > 0;
84+
const hasTfaToken = typeof tfaToken === 'string' && tfaToken.length > 0;
3385
log.debug('Starting login process', {
3486
module: 'auth-handler',
3587
email,
@@ -49,11 +101,9 @@ class AuthHandler {
49101
log.debug('Adding TFA token to login payload', { module: 'auth-handler' });
50102
}
51103

52-
const hasCredentials = !!password;
53-
const hasTfaTokenPresent = !!tfaToken;
54104
log.debug('Making login API call', {
55105
module: 'auth-handler',
56-
payload: { email, hasCredentials, hasTfaTokenPresent },
106+
payload: { email, hasCredentials, hasTfaToken },
57107
});
58108

59109
this._client
@@ -69,46 +119,24 @@ class AuthHandler {
69119
log.debug('Login successful, user found', { module: 'auth-handler', userEmail: result.user.email });
70120
resolve(result.user as User);
71121
} else if (result.error_code === 294) {
72-
log.debug('TFA required, requesting OTP channel', { module: 'auth-handler' });
73-
const otpChannel = await askOTPChannel();
74-
log.debug(`OTP channel selected: ${otpChannel}`, { module: 'auth-handler' });
75-
76-
// need to send sms to the mobile
77-
if (otpChannel === 'sms') {
78-
log.debug('Sending SMS OTP request', { module: 'auth-handler' });
79-
try {
80-
await this._client.axiosInstance.post('/user/request_token_sms', { user: loginPayload });
81-
log.debug('SMS OTP request successful', { module: 'auth-handler' });
82-
cliux.print('CLI_AUTH_LOGIN_SECURITY_CODE_SEND_SUCCESS');
83-
} catch (error) {
84-
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
85-
const err = cliErrorHandler.classifyError(error);
86-
reject(err);
87-
return;
88-
}
89-
}
90-
91-
log.debug('Requesting OTP input from user', { module: 'auth-handler' });
92-
const tfToken = await askOTP();
93-
log.debug('OTP received, retrying login', { module: 'auth-handler' });
122+
const tfToken = await this.handleOTPFlow(tfaToken, loginPayload);
94123

95124
try {
96125
resolve(await this.login(email, password, tfToken));
97126
} catch (error) {
98127
log.debug('Login with TFA token failed', { module: 'auth-handler', error });
99-
const err = cliErrorHandler.classifyError(error);
100-
reject(err);
101-
return;
128+
cliux.print('CLI_AUTH_2FA_FAILED', { color: 'red' });
129+
reject(error);
102130
}
103131
} else {
104132
log.debug('Login failed - no user found', { module: 'auth-handler', result });
105-
reject(new CLIError({ message: 'No user found with the credentials' }));
133+
reject(new Error(messageHandler.parse('CLI_AUTH_LOGIN_NO_USER')));
106134
}
107135
})
108136
.catch((error: any) => {
109-
log.debug('Login API call failed', { module: 'auth-handler', error: error.message || error });
110-
const err = cliErrorHandler.classifyError(error);
111-
reject(err);
137+
log.debug('Login API call failed', { module: 'auth-handler', error: error?.errorMessage || error });
138+
cliux.print('CLI_AUTH_LOGIN_FAILED', { color: 'yellow' });
139+
handleAndLogError(error, { module: 'auth-handler' });
112140
});
113141
} else {
114142
const hasEmail = !!email;
@@ -118,7 +146,8 @@ class AuthHandler {
118146
hasEmail,
119147
hasCredentials,
120148
});
121-
reject(new CLIError({ message: 'No credential found to login' }));
149+
log.debug('Login failed - missing credentials', { module: 'auth-handler', hasEmail, hasCredentials });
150+
reject(new Error(messageHandler.parse('CLI_AUTH_LOGIN_NO_CREDENTIALS')));
122151
}
123152
});
124153
}
@@ -143,12 +172,12 @@ class AuthHandler {
143172
})
144173
.catch((error: Error) => {
145174
log.debug('Logout API call failed', { module: 'auth-handler', error: error.message });
146-
const err = cliErrorHandler.classifyError(error);
147-
reject(err);
175+
cliux.print('CLI_AUTH_LOGOUT_FAILED', { color: 'yellow' });
176+
reject(error);
148177
});
149178
} else {
150179
log.debug('Logout failed - no auth token provided', { module: 'auth-handler' });
151-
reject(new CLIError({ message: 'No auth token found to logout' }));
180+
reject(new Error(messageHandler.parse('CLI_AUTH_LOGOUT_NO_TOKEN')));
152181
}
153182
});
154183
}
@@ -173,12 +202,12 @@ class AuthHandler {
173202
})
174203
.catch((error: Error) => {
175204
log.debug('Token validation failed', { module: 'auth-handler', error: error.message });
176-
const err = cliErrorHandler.classifyError(error);
177-
reject(err);
205+
cliux.print('CLI_AUTH_TOKEN_VALIDATION_FAILED', { color: 'yellow' });
206+
handleAndLogError(error, { module: 'auth-handler' });
178207
});
179208
} else {
180209
log.debug('Token validation failed - no auth token provided', { module: 'auth-handler' });
181-
reject(new CLIError({ message: 'No auth token found to validate' }));
210+
reject(new Error(messageHandler.parse('CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN')));
182211
}
183212
});
184213
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { default as authHandler } from './auth-handler';
2+
export { default as mfaHandler } from './mfa-handler';
23
export * as interactive from './interactive';
34
export * as tokenValidation from './tokens-validation';

packages/contentstack-auth/src/utils/interactive.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const askOTPChannel = async (): Promise<string> => {
1717
name: 'otpChannel',
1818
message: 'CLI_AUTH_LOGIN_ASK_CHANNEL_FOR_OTP',
1919
choices: [
20-
{ name: 'Authy App', value: 'authy' },
20+
{ name: 'Authenticator App', value: 'authenticator_app' },
2121
{ name: 'SMS', value: 'sms' },
2222
],
2323
});
@@ -45,8 +45,8 @@ export const askTokenType = async (): Promise<string> => {
4545
name: 'tokenType',
4646
message: 'CLI_SELECT_TOKEN_TYPE',
4747
choices: [
48-
{ name: 'Management Token', value: 'management'},
49-
{ name: 'Delivery Token', value: 'delivery'},
50-
]
48+
{ name: 'Management Token', value: 'management' },
49+
{ name: 'Delivery Token', value: 'delivery' },
50+
],
5151
});
52-
}
52+
};

0 commit comments

Comments
 (0)