Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
fileignoreconfig:
- filename: pnpm-lock.yaml
checksum: b8dc082b59f03873ab00adddd07df399fc74e90491f50ee96297467016fc9f20
checksum: fe9ac61ec17596aed093bd8ae333537962f9256a4a6c7f018a7f628a69f8cd22
version: '1.0'
6 changes: 3 additions & 3 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": "2.0.0-beta.9",
"version": "2.0.0-beta.10",
"author": "Contentstack",
"bugs": "https://github.com/contentstack/cli/issues",
"scripts": {
Expand All @@ -22,8 +22,8 @@
"test:unit:report": "nyc --extension .ts mocha --forbid-only \"test/unit/**/*.test.ts\""
},
"dependencies": {
"@contentstack/cli-command": "~2.0.0-beta.4",
"@contentstack/cli-utilities": "~2.0.0-beta.4",
"@contentstack/cli-command": "~2.0.0-beta.5",
"@contentstack/cli-utilities": "~2.0.0-beta.5",
"@oclif/core": "^4.3.0",
"@oclif/plugin-help": "^6.2.28",
"otplib": "^12.0.1"
Expand Down
4 changes: 2 additions & 2 deletions packages/contentstack-command/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@contentstack/cli-command",
"description": "Contentstack CLI plugin for configuration",
"version": "2.0.0-beta.4",
"version": "2.0.0-beta.5",
"author": "Contentstack",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand All @@ -20,7 +20,7 @@
"test:unit": "mocha --timeout 10000 --forbid-only \"test/unit/**/*.test.ts\""
},
"dependencies": {
"@contentstack/cli-utilities": "~2.0.0-beta.4",
"@contentstack/cli-utilities": "~2.0.0-beta.5",
"contentstack": "^3.25.3",
"@oclif/core": "^4.3.0",
"@oclif/plugin-help": "^6.2.28"
Expand Down
6 changes: 3 additions & 3 deletions packages/contentstack-config/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@contentstack/cli-config",
"description": "Contentstack CLI plugin for configuration",
"version": "2.0.0-beta.5",
"version": "2.0.0-beta.6",
"author": "Contentstack",
"scripts": {
"build": "pnpm compile && oclif manifest && oclif readme",
Expand All @@ -21,8 +21,8 @@
"test:unit:report": "nyc --extension .ts mocha --forbid-only \"test/unit/**/*.test.ts\""
},
"dependencies": {
"@contentstack/cli-command": "~2.0.0-beta.4",
"@contentstack/cli-utilities": "~2.0.0-beta.4",
"@contentstack/cli-command": "~2.0.0-beta.5",
"@contentstack/cli-utilities": "~2.0.0-beta.5",
"@contentstack/utils": "~1.7.0",
"@oclif/core": "^4.8.1",
"@oclif/plugin-help": "^6.2.28",
Expand Down
4 changes: 2 additions & 2 deletions packages/contentstack-utilities/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/cli-utilities",
"version": "2.0.0-beta.4",
"version": "2.0.0-beta.5",
"description": "Utilities for contentstack projects",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down Expand Up @@ -33,7 +33,7 @@
"author": "contentstack",
"license": "MIT",
"dependencies": {
"@contentstack/management": "~1.27.6",
"@contentstack/management": "~1.29.1",
"@contentstack/marketplace-sdk": "^1.5.0",
"@oclif/core": "^4.3.0",
"axios": "^1.13.5",
Expand Down
61 changes: 32 additions & 29 deletions packages/contentstack-utilities/src/auth-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ class AuthHandler {
private allAuthConfigItems: any;
private oauthHandler: any;
private managementAPIClient: ContentstackClient;
/** True while an OAuth access-token refresh is running (for logging/diagnostics; correctness uses `oauthRefreshInFlight`). */
private isRefreshingToken: boolean = false; // Flag to track if a refresh operation is in progress
/** Serialize OAuth refresh so concurrent API calls await the same refresh instead of proceeding with a stale token. */
private oauthRefreshInFlight: Promise<void> | null = null;
private cmaHost: string;

set host(contentStackHost) {
Expand Down Expand Up @@ -376,42 +379,42 @@ class AuthHandler {
checkExpiryAndRefresh = (force: boolean = false) => this.compareOAuthExpiry(force);

async compareOAuthExpiry(force: boolean = false) {
// Avoid recursive refresh operations
if (this.isRefreshingToken) {
cliux.print('Refresh operation already in progress');
return Promise.resolve();
}
const oauthDateTime = configHandler.get(this.oauthDateTimeKeyName);
const authorisationType = configHandler.get(this.authorisationTypeKeyName);
if (oauthDateTime && authorisationType === this.authorisationTypeOAUTHValue) {
const now = new Date();
const oauthDate = new Date(oauthDateTime);
const oauthValidUpto = new Date();
oauthValidUpto.setTime(oauthDate.getTime() + 59 * 60 * 1000);
if (force) {
cliux.print('Forcing token refresh...');
return this.refreshToken();
} else {
if (oauthValidUpto > now) {
return Promise.resolve();
} else {
cliux.print('Token expired, refreshing the token');
// Set the flag before refreshing the token
this.isRefreshingToken = true;

try {
await this.refreshToken();
} catch (error) {
cliux.error('Error refreshing token');
throw error;
} finally {
// Reset the flag after refresh operation is completed
this.isRefreshingToken = false;
}
const oauthValidUpto = new Date(oauthDate.getTime() + 59 * 60 * 1000);
const tokenExpired = oauthValidUpto <= now;
const shouldRefresh = force || tokenExpired;

return Promise.resolve();
}
if (!shouldRefresh) {
return Promise.resolve();
}

if (this.oauthRefreshInFlight) {
return this.oauthRefreshInFlight;
}

this.isRefreshingToken = true;
this.oauthRefreshInFlight = (async () => {
try {
if (force) {
cliux.print('Forcing token refresh...');
} else {
cliux.print('Token expired, refreshing the token');
}
await this.refreshToken();
} catch (error) {
cliux.error('Error refreshing token');
throw error;
} finally {
this.isRefreshingToken = false;
this.oauthRefreshInFlight = null;
}
})();

return this.oauthRefreshInFlight;
} else {
cliux.print('No OAuth configuration set.');
this.unsetConfigData();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,6 @@ export default class CLIProgressManager {
return;
}

if (configHandler.get('log')?.showConsoleLogs) {
return;
}

// Apply strategy-based corrections before printing
CLIProgressManager.applyStrategyCorrections();

Expand Down Expand Up @@ -541,7 +537,11 @@ export default class CLIProgressManager {
: getChalk().cyan(`${this.successCount}✓ ${this.failureCount}✗`);

const labelColor =
totalProcessed >= this.total ? (this.failureCount === 0 ? getChalk().green : getChalk().yellow) : getChalk().cyan;
totalProcessed >= this.total
? this.failureCount === 0
? getChalk().green
: getChalk().yellow
: getChalk().cyan;

const formattedName = this.formatModuleName(this.moduleName);
const displayName = formattedName.length > 20 ? formattedName.substring(0, 17) + '...' : formattedName;
Expand Down Expand Up @@ -617,10 +617,10 @@ export default class CLIProgressManager {
process.status === 'completed'
? '✓'
: process.status === 'failed'
? '✗'
: process.status === 'active'
? '●'
: '○';
? '✗'
: process.status === 'active'
? '●'
: '○';

this.log(
` ${status} ${processName}: ${process.successCount}✓ ${process.failureCount}✗ (${process.current}/${process.total})`,
Expand Down
72 changes: 49 additions & 23 deletions packages/contentstack-utilities/test/unit/auth-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ describe('Auth Handler', () => {

beforeEach(() => {
sandbox = createSandbox();
authHandler.oauthRefreshInFlight = null;
authHandler.isRefreshingToken = false;
configHandlerGetStub = sandbox.stub(configHandler, 'get');
cliuxPrintStub = sandbox.stub(cliux, 'print');
refreshTokenStub = sandbox.stub(authHandler, 'refreshToken').resolves();
Expand All @@ -467,40 +469,64 @@ describe('Auth Handler', () => {
});

it('should resolve if the OAuth token is valid and not expired', async () => {
const expectedOAuthDateTime = '2023-05-30T12:00:00Z';
const expectedAuthorisationType = 'oauth';
const now = new Date('2023-05-30T12:30:00Z');
const expectedOAuthDateTime = new Date(Date.now() - 30 * 60 * 1000).toISOString();
const expectedAuthorisationType = 'OAUTH';

configHandlerGetStub.withArgs(authHandler.oauthDateTimeKeyName).returns(expectedOAuthDateTime);
configHandlerGetStub.withArgs(authHandler.authorisationTypeKeyName).returns(expectedAuthorisationType);

sandbox.stub(Date, 'now').returns(now.getTime());
await authHandler.compareOAuthExpiry();
expect(cliuxPrintStub.called).to.be.false;
expect(refreshTokenStub.called).to.be.false;
expect(unsetConfigDataStub.called).to.be.false;
});

try {
await authHandler.compareOAuthExpiry();
} catch (error) {
expect(error).to.be.undefined;
expect(cliuxPrintStub.called).to.be.false;
expect(refreshTokenStub.called).to.be.false;
expect(unsetConfigDataStub.called).to.be.false;
}
it('should refresh when force is true even if token is not expired', async () => {
const expectedOAuthDateTime = new Date(Date.now() - 30 * 60 * 1000).toISOString();
const expectedAuthorisationType = 'OAUTH';

configHandlerGetStub.withArgs(authHandler.oauthDateTimeKeyName).returns(expectedOAuthDateTime);
configHandlerGetStub.withArgs(authHandler.authorisationTypeKeyName).returns(expectedAuthorisationType);

await authHandler.compareOAuthExpiry(true);
expect(cliuxPrintStub.calledOnceWithExactly('Forcing token refresh...')).to.be.true;
expect(refreshTokenStub.calledOnce).to.be.true;
expect(unsetConfigDataStub.called).to.be.false;
});

it('should resolve if force is true and refreshToken is called', async () => {
const expectedOAuthDateTime = '2023-05-30T12:00:00Z';
const expectedAuthorisationType = 'oauth';
it('should refresh when token is expired', async () => {
const expectedOAuthDateTime = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
const expectedAuthorisationType = 'OAUTH';

configHandlerGetStub.withArgs(authHandler.oauthDateTimeKeyName).returns(expectedOAuthDateTime);
configHandlerGetStub.withArgs(authHandler.authorisationTypeKeyName).returns(expectedAuthorisationType);

try {
await authHandler.compareOAuthExpiry();
} catch (error) {
expect(error).to.be.undefined;
expect(cliuxPrintStub.calledOnceWithExactly('Forcing token refresh...')).to.be.true;
expect(refreshTokenStub.calledOnce).to.be.true;
expect(unsetConfigDataStub.called).to.be.false;
}
await authHandler.compareOAuthExpiry(false);
expect(cliuxPrintStub.calledOnceWithExactly('Token expired, refreshing the token')).to.be.true;
expect(refreshTokenStub.calledOnce).to.be.true;
});

it('should run a single refresh when compareOAuthExpiry is called concurrently', async () => {
const expectedOAuthDateTime = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
const expectedAuthorisationType = 'OAUTH';
let resolveRefresh;
const refreshDone = new Promise((r) => {
resolveRefresh = r;
});

configHandlerGetStub.withArgs(authHandler.oauthDateTimeKeyName).returns(expectedOAuthDateTime);
configHandlerGetStub.withArgs(authHandler.authorisationTypeKeyName).returns(expectedAuthorisationType);

refreshTokenStub.callsFake(async () => {
await refreshDone;
});

const p1 = authHandler.compareOAuthExpiry(false);
const p2 = authHandler.compareOAuthExpiry(false);
resolveRefresh();
await Promise.all([p1, p2]);

expect(refreshTokenStub.callCount).to.equal(1);
});
});
});
Loading
Loading