Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
01161d2
Merge pull request #2056 from contentstack/staging
harshithad0703 Aug 11, 2025
cf5af13
Merge pull request #2059 from contentstack/main
harshithad0703 Aug 11, 2025
831dfec
Merge branch 'development' into fix/resolve-conflicts
harshithad0703 Aug 11, 2025
e244583
fix: Handled cases where the environment sub-folder may be missing to…
aman19K Aug 11, 2025
54d07bb
Merge pull request #2062 from contentstack/enh/DX-3419
cs-raj Aug 11, 2025
99cbfde
Merge branch 'development' into fix/resolve-conflicts
harshithad0703 Aug 12, 2025
838376c
Merge pull request #2061 from contentstack/fix/resolve-conflicts
harshithad0703 Aug 12, 2025
88d3485
chore: bump version to 1.44.3 in package.json for @contentstack/cli
harshithad0703 Aug 14, 2025
2d39114
Merge pull request #2066 from contentstack/fix/version-bump
harshithad0703 Aug 14, 2025
507d5f5
Merge pull request #2065 from contentstack/development
harshithad0703 Aug 14, 2025
f7b283d
Added MFA support in the Login command
cs-raj Aug 14, 2025
52b1dbb
Ui text updated
cs-raj Aug 14, 2025
431d90b
tsconfig update
cs-raj Aug 14, 2025
85de464
Test updated
cs-raj Aug 14, 2025
23f6861
Message Handler changes reverted
cs-raj Aug 14, 2025
e538b81
Removed the error
cs-raj Aug 14, 2025
908248f
Fixed formatting
cs-raj Aug 14, 2025
51d4ec2
Removed redundant code
cs-raj Aug 14, 2025
c6b6034
Merge pull request #2068 from contentstack/feat/release
cs-raj Aug 14, 2025
81bfe4d
Talisman Fix
cs-raj Aug 18, 2025
3a32bac
Merge pull request #2073 from contentstack/fix/secret
harshithad0703 Aug 18, 2025
6968358
Merge branch 'staging' into development
cs-raj Aug 18, 2025
03f9560
Merge pull request #2074 from contentstack/fix/dev
harshithad0703 Aug 18, 2025
1338654
Merge pull request #2072 from contentstack/development
harshithad0703 Aug 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
fileignoreconfig:
- filename: package-lock.json
checksum: c73d080bfbbf03bc2597891f956367bfd71f5f2f8d03525175426015265edc91
checksum: b01e61e606c0f2fd26cee8c27e00f5910973020afa72ddfb9d995f09dff6229f
- filename: pnpm-lock.yaml
checksum: 0ca85066946c49994a4353c9f64b8f380d5d2050194e3e57ad7ccd7faa030d36
checksum: e0310a5c5772a87bf606ee80722e29f9d0bc412ce10444f825a5c14dce4c1b88
- filename: packages/contentstack-import-setup/test/unit/backup-handler.test.ts
checksum: 0582d62b88834554cf12951c8690a73ef3ddbb78b82d2804d994cf4148e1ef93
- filename: packages/contentstack-import-setup/test/config.json
Expand Down Expand Up @@ -32,7 +32,29 @@ fileignoreconfig:
- filename: packages/contentstack-import-setup/test/unit/login-handler.test.ts
checksum: e549f9ca3a9aae0d93b7284f7e771d55c0610725ddcb4333612df2f215e92769
- filename: packages/contentstack/README.md
checksum: c09f6dc93702caff3adf689b501ec32586a16c865c1fe3a63b53ae645ca22349
checksum: f46084b199b3b0d7986b363c86a657570def71e5da29b948cc343eaf94ec7e97
- filename: packages/contentstack-import-setup/test/unit/modules/assets.test.ts
checksum: 449a5e3383631a6f78d1291aa3c28c91681879289398f0a933158fba5c5d5acf
version: "1.0"
- filename: packages/contentstack-auth/env.example
checksum: 72c9ed18a449c42b03ec54795898f6bad4e15d23a3d701c05b96fb17c3bbd93b
- filename: packages/contentstack-auth/test/integration/auth.test.ts
checksum: 9933a64d17d6d6dd7dd87ff210ce5e8a215bf36fac0cfd333894612ed10fb81b
- filename: packages/contentstack-auth/src/utils/mfa-handler.ts
checksum: ca9c34a3fe6c3b957debff987aefbceb641bf4954f15541d07d901f91e5ff014
- filename: packages/contentstack-auth/messages/index.json
checksum: 95856ad6273f17a9e853cda9c2cf0bdd782e47aeab93385e73ab870b5e814f89
- filename: packages/contentstack-auth/test/utils/auth-handler.test.ts
checksum: f88dded3a326f191844e39258e7fe390a72fefeb387d09c7f97e4e8aed520c97
- filename: packages/contentstack-auth/src/commands/auth/login.ts
checksum: 89204be8dfc1f670a568af992b54f34845e49bd4a8046c0cf041dd3759150718
- filename: packages/contentstack-auth/test/unit/commands/tokens-add.test.ts
checksum: 1e7247908e1887998210381c03caca93a3983e1c8967483464cf1c3bd3209cd1
- filename: packages/contentstack-auth/test/unit/commands/logout.test.ts
checksum: cd22dd04bd6a77cafa7dd0960cd4691201a3e228216d5a10041b8e39d7ebba1f
- filename: packages/contentstack-auth/src/utils/auth-handler.ts
checksum: 1261d02e8215da2db28557b77d6a8c8c604e11df88520e1cc5c8561e26bdd150
- filename: packages/contentstack-auth/test/unit/commands/login.test.ts
checksum: f93aa9b0c964608b60c88d4c72ff33840b58ec900297c4bae1f4ea365aa51048
- filename: packages/contentstack-auth/test/utils/mfa-handler.test.ts
checksum: b067f93cf0185d794e8419cc41e8fac96ed790dea8fc48dc083ee242ccacbd4d
version: "1.0"
780 changes: 393 additions & 387 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/contentstack-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ $ npm install -g @contentstack/cli-auth
$ csdx COMMAND
running command...
$ csdx (--version)
@contentstack/cli-auth/1.5.1 darwin-arm64 node-v22.13.1
@contentstack/cli-auth/1.6.0 darwin-arm64 node-v22.14.0
$ csdx --help [COMMAND]
USAGE
$ csdx COMMAND
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-auth/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ BRANCH_ENABLED_DELIVERY_TOKEN
BRANCH_DISABLED_DELIVERY_TOKEN
BRANCH_ENABLED_ENVIRONMENT
BRANCH_DISABLED_ENVIRONMENT
CONTENTSTACK_MFA_SECRET
13 changes: 12 additions & 1 deletion packages/contentstack-auth/messages/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,16 @@
"CLI_AUTH_TOKENS_VALIDATION_INVALID_API_KEY": "Invalid api key",
"CLI_AUTH_EXIT_PROCESS": "Exiting the process...",
"CLI_SELECT_TOKEN_TYPE": "Select the type of token to add",
"CLI_AUTH_ENTER_BRANCH": "Enter branch name"
"CLI_AUTH_MFA_INVALID_SECRET": "Invalid MFA secret format. Verify your authentication setup.",
"CLI_AUTH_MFA_GENERATION_FAILED": "Failed to generate MFA code. Switching to manual MFA code entry.",
"CLI_AUTH_MFA_DECRYPT_FAILED": "Failed to decrypt stored MFA secret. Try Resetting the MFA secret. Proceeding for Manual MFA code input.",
"CLI_AUTH_MFA_INVALID_CODE": "Invalid authentication code format.",
"CLI_AUTH_MFA_RECONFIGURE_HINT": "Consider reconfiguring MFA using config:mfa:add",
"CLI_AUTH_SMS_OTP_FAILED": "Failed to send SMS OTP. Try again or use a different two-factor authentication method.",
"CLI_AUTH_2FA_FAILED": "Two-factor authentication failed! Try again.",
"CLI_AUTH_LOGIN_NO_USER": "No user found with the provided credentials!",
"CLI_AUTH_LOGIN_NO_CREDENTIALS": "No credentials provided. Enter your email and password to log in.",
"CLI_AUTH_LOGOUT_NO_TOKEN": "No auth token found. Log in before logging out.",
"CLI_AUTH_TOKEN_VALIDATION_FAILED": "Token validation failed. Log in again.",
"CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN": "No auth token found. Log in to continue.."
}
5 changes: 3 additions & 2 deletions packages/contentstack-auth/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@contentstack/cli-auth",
"description": "Contentstack CLI plugin for authentication activities",
"version": "1.5.1",
"version": "1.6.0",
"author": "Contentstack",
"bugs": "https://github.com/contentstack/cli/issues",
"scripts": {
Expand All @@ -25,7 +25,8 @@
"@contentstack/cli-command": "~1.6.0",
"@contentstack/cli-utilities": "~1.13.1",
"@oclif/core": "^4.3.0",
"@oclif/plugin-help": "^6.2.28"
"@oclif/plugin-help": "^6.2.28",
"otplib": "^12.0.1"
},
"devDependencies": {
"@fancy-test/nock": "^0.1.1",
Expand Down
37 changes: 30 additions & 7 deletions packages/contentstack-auth/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
messageHandler,
} from '@contentstack/cli-utilities';
import { User } from '../../interfaces';
import { authHandler, interactive } from '../../utils';
import { authHandler, interactive, mfaHandler } from '../../utils';
import { BaseCommand } from '../../base-command';

export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
Expand Down Expand Up @@ -40,6 +40,7 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
required: false,
exclusive: ['oauth'],
}),

oauth: flags.boolean({
description: 'Enables single sign-on (SSO) in Contentstack CLI.',
required: false,
Expand All @@ -57,9 +58,9 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
log.debug('Initializing management API client', this.contextDetails);
const managementAPIClient = await managementSDKClient({ host: this.cmaHost, skipTokenValidity: true });
log.debug('Management API client initialized successfully', this.contextDetails);

const { flags: loginFlags } = await this.parse(LoginCommand);
log.debug('Token add flags parsed', {...this.contextDetails, flags: loginFlags});
log.debug('Token add flags parsed', { ...this.contextDetails, flags: loginFlags });

authHandler.client = managementAPIClient;
log.debug('Auth handler client set', this.contextDetails);
Expand All @@ -76,12 +77,22 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {
log.debug('Starting basic authentication flow', this.contextDetails);
const username = loginFlags?.username || (await interactive.askUsername());
const password = loginFlags?.password || (await interactive.askPassword());
log.debug('Credentials obtained', { ...this.contextDetails, hasUsername: !!username, hasPassword: !!password });
log.debug('Credentials obtained', {
...this.contextDetails,
hasUsername: !!username,
hasPassword: !!password,
});

await this.login(username, password);
}
} catch (error) {
log.debug('Login command failed', { ...this.contextDetails, error });
cliux.error('CLI_AUTH_LOGIN_FAILED');
log.debug('Login command failed', {
...this.contextDetails,
error,
});
if ((error?.message && error?.message.includes('2FA')) || error?.message.includes('MFA')) {
error.message = `${error.message}\nFor more information about MFA, visit: https://www.contentstack.com/docs/developers/security/multi-factor-authentication`;
}
handleAndLogError(error, { ...this.contextDetails });
process.exit();
}
Expand All @@ -92,7 +103,19 @@ export default class LoginCommand extends BaseCommand<typeof LoginCommand> {

try {
log.debug('Calling auth handler login', this.contextDetails);
const user: User = await authHandler.login(username, password);
let tfaToken: string | undefined;

try {
tfaToken = await mfaHandler.getMFACode();
if (tfaToken) {
log.debug('MFA token generated from stored configuration', this.contextDetails);
}
} catch (error) {
log.debug('Failed to generate MFA token from config', { ...this.contextDetails, error });
tfaToken = undefined;
}

const user: User = await authHandler.login(username, password, tfaToken);
log.debug('Auth handler login completed', {
...this.contextDetails,
hasUser: !!user,
Expand Down
113 changes: 71 additions & 42 deletions packages/contentstack-auth/src/utils/auth-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cliux, CLIError, log, cliErrorHandler } from '@contentstack/cli-utilities';
import { cliux, log, handleAndLogError, messageHandler } from '@contentstack/cli-utilities';
import { User } from '../interfaces';
import { askOTPChannel, askOTP } from './interactive';

Expand Down Expand Up @@ -27,9 +27,61 @@ class AuthHandler {
* @returns {Promise} Promise object returns authtoken on success
* TBD: take out the otp implementation from login and create a new method/function to handle otp
*/
/**
* Handle the OTP flow for 2FA authentication
* @param tfaToken Optional pre-provided TFA token
* @param loginPayload Login payload containing user credentials
* @returns Promise<string> The TFA token to use for authentication
*/
private async handleOTPFlow(tfaToken?: string, loginPayload?: any): Promise<string> {
try {
if (tfaToken) {
log.info('Using provided TFA token', { module: 'auth-handler' });
return tfaToken;
}

log.debug('2FA required, requesting OTP channel', { module: 'auth-handler' });
const otpChannel = await askOTPChannel();
log.debug(`OTP channel selected: ${otpChannel}`, { module: 'auth-handler' });

if (otpChannel === 'sms') {
try {
await this.requestSMSOTP(loginPayload);
} catch (error) {
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
cliux.print('CLI_AUTH_SMS_OTP_FAILED', { color: 'red' });
throw error;
}
}

log.debug('Requesting OTP input', { module: 'auth-handler', channel: otpChannel });
return await askOTP();
} catch (error) {
log.debug('2FA flow failed', { module: 'auth-handler', error });
throw error;
}
}

/**
* Request SMS OTP for 2FA authentication
* @param loginPayload Login payload containing user credentials
* @throws CLIError if SMS request fails
*/
private async requestSMSOTP(loginPayload: any): Promise<void> {
log.debug('Sending SMS OTP request', { module: 'auth-handler' });
try {
await this._client.axiosInstance.post('/user/request_token_sms', { user: loginPayload });
log.debug('SMS OTP request successful', { module: 'auth-handler' });
cliux.print('CLI_AUTH_LOGIN_SECURITY_CODE_SEND_SUCCESS');
} catch (error) {
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
throw error;
}
}

async login(email: string, password: string, tfaToken?: string): Promise<User> {
const hasCredentials = !!password;
const hasTfaToken = !!tfaToken;
const hasCredentials = typeof password === 'string' && password.length > 0;
const hasTfaToken = typeof tfaToken === 'string' && tfaToken.length > 0;
log.debug('Starting login process', {
module: 'auth-handler',
email,
Expand All @@ -49,11 +101,9 @@ class AuthHandler {
log.debug('Adding TFA token to login payload', { module: 'auth-handler' });
}

const hasCredentials = !!password;
const hasTfaTokenPresent = !!tfaToken;
log.debug('Making login API call', {
module: 'auth-handler',
payload: { email, hasCredentials, hasTfaTokenPresent },
payload: { email, hasCredentials, hasTfaToken },
});

this._client
Expand All @@ -69,46 +119,24 @@ class AuthHandler {
log.debug('Login successful, user found', { module: 'auth-handler', userEmail: result.user.email });
resolve(result.user as User);
} else if (result.error_code === 294) {
log.debug('TFA required, requesting OTP channel', { module: 'auth-handler' });
const otpChannel = await askOTPChannel();
log.debug(`OTP channel selected: ${otpChannel}`, { module: 'auth-handler' });

// need to send sms to the mobile
if (otpChannel === 'sms') {
log.debug('Sending SMS OTP request', { module: 'auth-handler' });
try {
await this._client.axiosInstance.post('/user/request_token_sms', { user: loginPayload });
log.debug('SMS OTP request successful', { module: 'auth-handler' });
cliux.print('CLI_AUTH_LOGIN_SECURITY_CODE_SEND_SUCCESS');
} catch (error) {
log.debug('SMS OTP request failed', { module: 'auth-handler', error });
const err = cliErrorHandler.classifyError(error);
reject(err);
return;
}
}

log.debug('Requesting OTP input from user', { module: 'auth-handler' });
const tfToken = await askOTP();
log.debug('OTP received, retrying login', { module: 'auth-handler' });
const tfToken = await this.handleOTPFlow(tfaToken, loginPayload);

try {
resolve(await this.login(email, password, tfToken));
} catch (error) {
log.debug('Login with TFA token failed', { module: 'auth-handler', error });
const err = cliErrorHandler.classifyError(error);
reject(err);
return;
cliux.print('CLI_AUTH_2FA_FAILED', { color: 'red' });
reject(error);
}
} else {
log.debug('Login failed - no user found', { module: 'auth-handler', result });
reject(new CLIError({ message: 'No user found with the credentials' }));
reject(new Error(messageHandler.parse('CLI_AUTH_LOGIN_NO_USER')));
}
})
.catch((error: any) => {
log.debug('Login API call failed', { module: 'auth-handler', error: error.message || error });
const err = cliErrorHandler.classifyError(error);
reject(err);
log.debug('Login API call failed', { module: 'auth-handler', error: error?.errorMessage || error });
cliux.print('CLI_AUTH_LOGIN_FAILED', { color: 'yellow' });
handleAndLogError(error, { module: 'auth-handler' });
});
} else {
const hasEmail = !!email;
Expand All @@ -118,7 +146,8 @@ class AuthHandler {
hasEmail,
hasCredentials,
});
reject(new CLIError({ message: 'No credential found to login' }));
log.debug('Login failed - missing credentials', { module: 'auth-handler', hasEmail, hasCredentials });
reject(new Error(messageHandler.parse('CLI_AUTH_LOGIN_NO_CREDENTIALS')));
}
});
}
Expand All @@ -143,12 +172,12 @@ class AuthHandler {
})
.catch((error: Error) => {
log.debug('Logout API call failed', { module: 'auth-handler', error: error.message });
const err = cliErrorHandler.classifyError(error);
reject(err);
cliux.print('CLI_AUTH_LOGOUT_FAILED', { color: 'yellow' });
reject(error);
});
} else {
log.debug('Logout failed - no auth token provided', { module: 'auth-handler' });
reject(new CLIError({ message: 'No auth token found to logout' }));
reject(new Error(messageHandler.parse('CLI_AUTH_LOGOUT_NO_TOKEN')));
}
});
}
Expand All @@ -173,12 +202,12 @@ class AuthHandler {
})
.catch((error: Error) => {
log.debug('Token validation failed', { module: 'auth-handler', error: error.message });
const err = cliErrorHandler.classifyError(error);
reject(err);
cliux.print('CLI_AUTH_TOKEN_VALIDATION_FAILED', { color: 'yellow' });
handleAndLogError(error, { module: 'auth-handler' });
});
} else {
log.debug('Token validation failed - no auth token provided', { module: 'auth-handler' });
reject(new CLIError({ message: 'No auth token found to validate' }));
reject(new Error(messageHandler.parse('CLI_AUTH_TOKEN_VALIDATION_NO_TOKEN')));
}
});
}
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-auth/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as authHandler } from './auth-handler';
export { default as mfaHandler } from './mfa-handler';
export * as interactive from './interactive';
export * as tokenValidation from './tokens-validation';
10 changes: 5 additions & 5 deletions packages/contentstack-auth/src/utils/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const askOTPChannel = async (): Promise<string> => {
name: 'otpChannel',
message: 'CLI_AUTH_LOGIN_ASK_CHANNEL_FOR_OTP',
choices: [
{ name: 'Authy App', value: 'authy' },
{ name: 'Authenticator App', value: 'authenticator_app' },
{ name: 'SMS', value: 'sms' },
],
});
Expand Down Expand Up @@ -45,8 +45,8 @@ export const askTokenType = async (): Promise<string> => {
name: 'tokenType',
message: 'CLI_SELECT_TOKEN_TYPE',
choices: [
{ name: 'Management Token', value: 'management'},
{ name: 'Delivery Token', value: 'delivery'},
]
{ name: 'Management Token', value: 'management' },
{ name: 'Delivery Token', value: 'delivery' },
],
});
}
};
Loading
Loading