Skip to content
Open
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
8 changes: 4 additions & 4 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",
"version": "2.0.1",
"author": "Contentstack",
"bugs": "https://github.com/contentstack/cli/issues",
"scripts": {
Expand All @@ -15,14 +15,14 @@
"lint": "eslint src/**/*.ts"
},
"dependencies": {
"@contentstack/cli-command": "~2.0.0",
"@contentstack/cli-utilities": "~2.0.0",
"@contentstack/cli-command": "~2.0.1",
"@contentstack/cli-utilities": "~2.0.1",
"@oclif/core": "^4.11.14",
"otplib": "^12.0.1"
},
"overrides": {
"@oclif/core": {
"picomatch": "^4.0.4"
"picomatch": "^4.0.7"
}
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 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",
"version": "2.0.1",
"author": "Contentstack",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand All @@ -14,13 +14,13 @@
"lint": "eslint src/**/*.ts"
},
"dependencies": {
"@contentstack/cli-utilities": "~2.0.0",
"@contentstack/cli-utilities": "~2.0.1",
"contentstack": "^3.27.0",
"@oclif/core": "^4.11.14"
},
"overrides": {
"@oclif/core": {
"picomatch": "^4.0.4"
"picomatch": "^4.0.7"
}
},
"devDependencies": {
Expand Down
8 changes: 4 additions & 4 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",
"version": "2.0.1",
"author": "Contentstack",
"scripts": {
"build": "pnpm compile && oclif manifest && oclif readme",
Expand All @@ -14,14 +14,14 @@
"lint": "eslint src/**/*.ts"
},
"dependencies": {
"@contentstack/cli-command": "~2.0.0",
"@contentstack/cli-utilities": "~2.0.0",
"@contentstack/cli-command": "~2.0.1",
"@contentstack/cli-utilities": "~2.0.1",
"@contentstack/utils": "~1.9.1",
"@oclif/core": "^4.11.14"
},
"overrides": {
"@oclif/core": {
"picomatch": "^4.0.4"
"picomatch": "^4.0.7"
}
},
"devDependencies": {
Expand Down
12 changes: 6 additions & 6 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",
"version": "2.0.1",
"description": "Utilities for contentstack projects",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down Expand Up @@ -28,11 +28,11 @@
"author": "contentstack",
"license": "MIT",
"dependencies": {
"@contentstack/management": "~1.30.3",
"@contentstack/marketplace-sdk": "^1.5.2",
"@contentstack/management": "~1.31.1",
"@contentstack/marketplace-sdk": "^1.5.4",
"@contentstack/utils": "~1.9.1",
"@oclif/core": "^4.11.14",
"axios": "^1.18.1",
"axios": "^1.20.0",
"chalk": "^5.6.2",
"cli-cursor": "^3.1.0",
"cli-progress": "^3.12.0",
Expand All @@ -43,7 +43,7 @@
"inquirer": "12.11.1",
"inquirer-search-checkbox": "^1.0.0",
"inquirer-search-list": "^1.2.6",
"js-yaml": "^4.3.0",
"js-yaml": "^4.3.2",
"klona": "^2.0.6",
"lodash": "^4.18.1",
"mkdirp": "^1.0.4",
Expand All @@ -62,7 +62,7 @@
},
"overrides": {
"@oclif/core": {
"picomatch": "^4.0.4"
"picomatch": "^4.0.7"
}
},
"devDependencies": {
Expand Down
19 changes: 16 additions & 3 deletions packages/contentstack-utilities/src/auth-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ class AuthHandler {
async initSDK() {
// Ensure we have a valid host for the SDK initialization
const host = this._host || this.getCmaHost();
this.managementAPIClient = await managementSDKClient({ host });
// skipTokenValidity: true avoids calling compareOAuthExpiry here, which would deadlock
// when initSDK is itself called from inside a compareOAuthExpiry refresh cycle.
this.managementAPIClient = await managementSDKClient({ host, skipTokenValidity: true });
this.oauthHandler = this.managementAPIClient.oauth({
appId: this.OAuthAppId,
clientId: this.OAuthClientId,
Expand Down Expand Up @@ -397,17 +399,28 @@ class AuthHandler {
}

this.isRefreshingToken = true;
this.oauthRefreshInFlight = (async () => {
// Set the guard promise BEFORE starting the async work so re-entrant synchronous
// callers (e.g. compareOAuthExpiry invoked again via refreshToken → initSDK →
// managementSDKClient → createAPIClient) see a non-null oauthRefreshInFlight and
// return early instead of starting another recursive refresh cycle.
let _resolve: () => void;
let _reject: (err: unknown) => void;
this.oauthRefreshInFlight = new Promise<void>((res, rej) => {
_resolve = res;
_reject = rej;
});
(async () => {
try {
if (force) {
cliux.print('Forcing token refresh...');
} else {
cliux.print('Token expired, refreshing the token');
}
await this.refreshToken();
_resolve();
} catch (error) {
cliux.error('Error refreshing token');
throw error;
_reject(error);
} finally {
this.isRefreshingToken = false;
this.oauthRefreshInFlight = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class AuthenticationHandler {
if (error.response && error.response.status) {
switch (error.response.status) {
case 401:
if (maxRetryCount >= 2) {
const errorDetails = formatError(error);
ux.print(`Authentication failed after token refresh: ${errorDetails}`, { color: 'red' });
return;
}
// NOTE: Refresh the token if the type is OAuth.
const region: { cma: string; name: string; cda: string } = configHandler.get('region') || {};
if (region?.cma) {
Expand All @@ -67,12 +72,11 @@ class AuthenticationHandler {
hostName = hostName || region.cma;
const refreshed = await this.refreshToken(hostName);
if (refreshed) {
return this.refreshAccessToken(error, maxRetryCount); // Retry after refreshing the token
return this.refreshAccessToken(error, maxRetryCount + 1);
}

const errorDetails = formatError(error);
ux.print(`Authentication failed: ${errorDetails}`, { color: 'red' });
// For Basic Auth, exit immediately without retrying
return;
}
break;
Expand Down
30 changes: 30 additions & 0 deletions packages/contentstack-utilities/test/unit/auth-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,5 +543,35 @@ describe('Auth Handler', () => {

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

it('should not recurse when compareOAuthExpiry is re-entered synchronously during refreshToken (DX-10477)', async () => {
// Regression test for the synchronous re-entrant recursion bug.
// Root cause: managementSDKClient → createAPIClient → compareOAuthExpiry is called
// synchronously from inside refreshToken → initSDK, before the oauthRefreshInFlight
// guard was assigned. This caused an unbounded recursive loop and RangeError stack overflow.
// Fix: oauthRefreshInFlight is now set via new Promise() BEFORE the async IIFE starts,
// so any synchronous re-entrant call sees the guard and returns the existing promise.
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);

refreshTokenStub.callsFake(function () {
// Simulate the synchronous re-entrant call from initSDK → managementSDKClient → createAPIClient.
// With the old code, oauthRefreshInFlight was null here → new IIFE → infinite recursion.
// With the fix, oauthRefreshInFlight is already set → returns the same in-flight promise.
const reentrantPromise = authHandler.compareOAuthExpiry(false);
expect(reentrantPromise).to.equal((authHandler as any).oauthRefreshInFlight);
return Promise.resolve();
});

await authHandler.compareOAuthExpiry(false);

// "Token expired, refreshing the token" must print exactly once — not on every recursive frame.
expect(cliuxPrintStub.calledOnceWithExactly('Token expired, refreshing the token')).to.be.true;
// refreshToken must be called exactly once — not infinitely.
expect(refreshTokenStub.callCount).to.equal(1);
});
});
});
8 changes: 4 additions & 4 deletions packages/contentstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@
"@contentstack/cli-audit": "~2.0.0",
"@contentstack/cli-cm-export": "~2.0.0",
"@contentstack/cli-cm-import": "~2.0.0",
"@contentstack/cli-auth": "~2.0.0",
"@contentstack/cli-auth": "~2.0.1",
"@contentstack/cli-bulk-operations": "^2.0.0",
"@contentstack/cli-cm-bootstrap": "~2.0.0",
"@contentstack/cli-cm-branches": "~2.0.0",
"@contentstack/cli-cm-clone": "~2.0.0",
"@contentstack/cli-cm-export-to-csv": "~2.0.0",
"@contentstack/cli-cm-import-setup": "~2.0.0",
"@contentstack/cli-cm-seed": "~2.0.0",
"@contentstack/cli-command": "~2.0.0",
"@contentstack/cli-config": "~2.0.0",
"@contentstack/cli-command": "~2.0.1",
"@contentstack/cli-config": "~2.0.1",
"@contentstack/cli-migration": "~2.0.0",
"@contentstack/cli-utilities": "~2.0.0",
"@contentstack/cli-utilities": "~2.0.1",
"@contentstack/cli-variants": "~2.0.0",
"@contentstack/management": "~1.30.4",
"@contentstack/utils": "~1.9.1",
Expand Down
Loading
Loading