Skip to content

Commit aeaa716

Browse files
Merge pull request #2501 from contentstack/v2-dev
DX | 30-03-2026 | Release | v2-beta
2 parents 78c23f3 + f6e6768 commit aeaa716

File tree

9 files changed

+137
-84
lines changed

9 files changed

+137
-84
lines changed

.talismanrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
fileignoreconfig:
22
- filename: pnpm-lock.yaml
3-
checksum: b8dc082b59f03873ab00adddd07df399fc74e90491f50ee96297467016fc9f20
3+
checksum: fe9ac61ec17596aed093bd8ae333537962f9256a4a6c7f018a7f628a69f8cd22
44
version: '1.0'

packages/contentstack-auth/package.json

Lines changed: 3 additions & 3 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": "2.0.0-beta.9",
4+
"version": "2.0.0-beta.10",
55
"author": "Contentstack",
66
"bugs": "https://github.com/contentstack/cli/issues",
77
"scripts": {
@@ -22,8 +22,8 @@
2222
"test:unit:report": "nyc --extension .ts mocha --forbid-only \"test/unit/**/*.test.ts\""
2323
},
2424
"dependencies": {
25-
"@contentstack/cli-command": "~2.0.0-beta.4",
26-
"@contentstack/cli-utilities": "~2.0.0-beta.4",
25+
"@contentstack/cli-command": "~2.0.0-beta.5",
26+
"@contentstack/cli-utilities": "~2.0.0-beta.5",
2727
"@oclif/core": "^4.3.0",
2828
"@oclif/plugin-help": "^6.2.28",
2929
"otplib": "^12.0.1"

packages/contentstack-command/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@contentstack/cli-command",
33
"description": "Contentstack CLI plugin for configuration",
4-
"version": "2.0.0-beta.4",
4+
"version": "2.0.0-beta.5",
55
"author": "Contentstack",
66
"main": "lib/index.js",
77
"types": "lib/index.d.ts",
@@ -20,7 +20,7 @@
2020
"test:unit": "mocha --timeout 10000 --forbid-only \"test/unit/**/*.test.ts\""
2121
},
2222
"dependencies": {
23-
"@contentstack/cli-utilities": "~2.0.0-beta.4",
23+
"@contentstack/cli-utilities": "~2.0.0-beta.5",
2424
"contentstack": "^3.25.3",
2525
"@oclif/core": "^4.3.0",
2626
"@oclif/plugin-help": "^6.2.28"

packages/contentstack-config/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@contentstack/cli-config",
33
"description": "Contentstack CLI plugin for configuration",
4-
"version": "2.0.0-beta.5",
4+
"version": "2.0.0-beta.6",
55
"author": "Contentstack",
66
"scripts": {
77
"build": "pnpm compile && oclif manifest && oclif readme",
@@ -21,8 +21,8 @@
2121
"test:unit:report": "nyc --extension .ts mocha --forbid-only \"test/unit/**/*.test.ts\""
2222
},
2323
"dependencies": {
24-
"@contentstack/cli-command": "~2.0.0-beta.4",
25-
"@contentstack/cli-utilities": "~2.0.0-beta.4",
24+
"@contentstack/cli-command": "~2.0.0-beta.5",
25+
"@contentstack/cli-utilities": "~2.0.0-beta.5",
2626
"@contentstack/utils": "~1.7.0",
2727
"@oclif/core": "^4.8.1",
2828
"@oclif/plugin-help": "^6.2.28",

packages/contentstack-utilities/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/cli-utilities",
3-
"version": "2.0.0-beta.4",
3+
"version": "2.0.0-beta.5",
44
"description": "Utilities for contentstack projects",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",
@@ -33,7 +33,7 @@
3333
"author": "contentstack",
3434
"license": "MIT",
3535
"dependencies": {
36-
"@contentstack/management": "~1.27.6",
36+
"@contentstack/management": "~1.29.1",
3737
"@contentstack/marketplace-sdk": "^1.5.0",
3838
"@oclif/core": "^4.3.0",
3939
"axios": "^1.13.5",

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

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ class AuthHandler {
3535
private allAuthConfigItems: any;
3636
private oauthHandler: any;
3737
private managementAPIClient: ContentstackClient;
38+
/** True while an OAuth access-token refresh is running (for logging/diagnostics; correctness uses `oauthRefreshInFlight`). */
3839
private isRefreshingToken: boolean = false; // Flag to track if a refresh operation is in progress
40+
/** Serialize OAuth refresh so concurrent API calls await the same refresh instead of proceeding with a stale token. */
41+
private oauthRefreshInFlight: Promise<void> | null = null;
3942
private cmaHost: string;
4043

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

378381
async compareOAuthExpiry(force: boolean = false) {
379-
// Avoid recursive refresh operations
380-
if (this.isRefreshingToken) {
381-
cliux.print('Refresh operation already in progress');
382-
return Promise.resolve();
383-
}
384382
const oauthDateTime = configHandler.get(this.oauthDateTimeKeyName);
385383
const authorisationType = configHandler.get(this.authorisationTypeKeyName);
386384
if (oauthDateTime && authorisationType === this.authorisationTypeOAUTHValue) {
387385
const now = new Date();
388386
const oauthDate = new Date(oauthDateTime);
389-
const oauthValidUpto = new Date();
390-
oauthValidUpto.setTime(oauthDate.getTime() + 59 * 60 * 1000);
391-
if (force) {
392-
cliux.print('Forcing token refresh...');
393-
return this.refreshToken();
394-
} else {
395-
if (oauthValidUpto > now) {
396-
return Promise.resolve();
397-
} else {
398-
cliux.print('Token expired, refreshing the token');
399-
// Set the flag before refreshing the token
400-
this.isRefreshingToken = true;
401-
402-
try {
403-
await this.refreshToken();
404-
} catch (error) {
405-
cliux.error('Error refreshing token');
406-
throw error;
407-
} finally {
408-
// Reset the flag after refresh operation is completed
409-
this.isRefreshingToken = false;
410-
}
387+
const oauthValidUpto = new Date(oauthDate.getTime() + 59 * 60 * 1000);
388+
const tokenExpired = oauthValidUpto <= now;
389+
const shouldRefresh = force || tokenExpired;
411390

412-
return Promise.resolve();
413-
}
391+
if (!shouldRefresh) {
392+
return Promise.resolve();
393+
}
394+
395+
if (this.oauthRefreshInFlight) {
396+
return this.oauthRefreshInFlight;
414397
}
398+
399+
this.isRefreshingToken = true;
400+
this.oauthRefreshInFlight = (async () => {
401+
try {
402+
if (force) {
403+
cliux.print('Forcing token refresh...');
404+
} else {
405+
cliux.print('Token expired, refreshing the token');
406+
}
407+
await this.refreshToken();
408+
} catch (error) {
409+
cliux.error('Error refreshing token');
410+
throw error;
411+
} finally {
412+
this.isRefreshingToken = false;
413+
this.oauthRefreshInFlight = null;
414+
}
415+
})();
416+
417+
return this.oauthRefreshInFlight;
415418
} else {
416419
cliux.print('No OAuth configuration set.');
417420
this.unsetConfigData();

packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,6 @@ export default class CLIProgressManager {
103103
return;
104104
}
105105

106-
if (configHandler.get('log')?.showConsoleLogs) {
107-
return;
108-
}
109-
110106
// Apply strategy-based corrections before printing
111107
CLIProgressManager.applyStrategyCorrections();
112108

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

543539
const labelColor =
544-
totalProcessed >= this.total ? (this.failureCount === 0 ? getChalk().green : getChalk().yellow) : getChalk().cyan;
540+
totalProcessed >= this.total
541+
? this.failureCount === 0
542+
? getChalk().green
543+
: getChalk().yellow
544+
: getChalk().cyan;
545545

546546
const formattedName = this.formatModuleName(this.moduleName);
547547
const displayName = formattedName.length > 20 ? formattedName.substring(0, 17) + '...' : formattedName;
@@ -617,10 +617,10 @@ export default class CLIProgressManager {
617617
process.status === 'completed'
618618
? '✓'
619619
: process.status === 'failed'
620-
? '✗'
621-
: process.status === 'active'
622-
? '●'
623-
: '○';
620+
? '✗'
621+
: process.status === 'active'
622+
? '●'
623+
: '○';
624624

625625
this.log(
626626
` ${status} ${processName}: ${process.successCount}${process.failureCount}✗ (${process.current}/${process.total})`,

packages/contentstack-utilities/test/unit/auth-handler.test.ts

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,8 @@ describe('Auth Handler', () => {
456456

457457
beforeEach(() => {
458458
sandbox = createSandbox();
459+
authHandler.oauthRefreshInFlight = null;
460+
authHandler.isRefreshingToken = false;
459461
configHandlerGetStub = sandbox.stub(configHandler, 'get');
460462
cliuxPrintStub = sandbox.stub(cliux, 'print');
461463
refreshTokenStub = sandbox.stub(authHandler, 'refreshToken').resolves();
@@ -467,40 +469,64 @@ describe('Auth Handler', () => {
467469
});
468470

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

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

477-
sandbox.stub(Date, 'now').returns(now.getTime());
478+
await authHandler.compareOAuthExpiry();
479+
expect(cliuxPrintStub.called).to.be.false;
480+
expect(refreshTokenStub.called).to.be.false;
481+
expect(unsetConfigDataStub.called).to.be.false;
482+
});
478483

479-
try {
480-
await authHandler.compareOAuthExpiry();
481-
} catch (error) {
482-
expect(error).to.be.undefined;
483-
expect(cliuxPrintStub.called).to.be.false;
484-
expect(refreshTokenStub.called).to.be.false;
485-
expect(unsetConfigDataStub.called).to.be.false;
486-
}
484+
it('should refresh when force is true even if token is not expired', async () => {
485+
const expectedOAuthDateTime = new Date(Date.now() - 30 * 60 * 1000).toISOString();
486+
const expectedAuthorisationType = 'OAUTH';
487+
488+
configHandlerGetStub.withArgs(authHandler.oauthDateTimeKeyName).returns(expectedOAuthDateTime);
489+
configHandlerGetStub.withArgs(authHandler.authorisationTypeKeyName).returns(expectedAuthorisationType);
490+
491+
await authHandler.compareOAuthExpiry(true);
492+
expect(cliuxPrintStub.calledOnceWithExactly('Forcing token refresh...')).to.be.true;
493+
expect(refreshTokenStub.calledOnce).to.be.true;
494+
expect(unsetConfigDataStub.called).to.be.false;
487495
});
488496

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

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

496-
try {
497-
await authHandler.compareOAuthExpiry();
498-
} catch (error) {
499-
expect(error).to.be.undefined;
500-
expect(cliuxPrintStub.calledOnceWithExactly('Forcing token refresh...')).to.be.true;
501-
expect(refreshTokenStub.calledOnce).to.be.true;
502-
expect(unsetConfigDataStub.called).to.be.false;
503-
}
504+
await authHandler.compareOAuthExpiry(false);
505+
expect(cliuxPrintStub.calledOnceWithExactly('Token expired, refreshing the token')).to.be.true;
506+
expect(refreshTokenStub.calledOnce).to.be.true;
507+
});
508+
509+
it('should run a single refresh when compareOAuthExpiry is called concurrently', async () => {
510+
const expectedOAuthDateTime = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
511+
const expectedAuthorisationType = 'OAUTH';
512+
let resolveRefresh;
513+
const refreshDone = new Promise((r) => {
514+
resolveRefresh = r;
515+
});
516+
517+
configHandlerGetStub.withArgs(authHandler.oauthDateTimeKeyName).returns(expectedOAuthDateTime);
518+
configHandlerGetStub.withArgs(authHandler.authorisationTypeKeyName).returns(expectedAuthorisationType);
519+
520+
refreshTokenStub.callsFake(async () => {
521+
await refreshDone;
522+
});
523+
524+
const p1 = authHandler.compareOAuthExpiry(false);
525+
const p2 = authHandler.compareOAuthExpiry(false);
526+
resolveRefresh();
527+
await Promise.all([p1, p2]);
528+
529+
expect(refreshTokenStub.callCount).to.equal(1);
504530
});
505531
});
506532
});

0 commit comments

Comments
 (0)