diff --git a/.talismanrc b/.talismanrc index e9954f9fc5..8dc433cbe5 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,4 +1,4 @@ fileignoreconfig: - filename: pnpm-lock.yaml - checksum: b8dc082b59f03873ab00adddd07df399fc74e90491f50ee96297467016fc9f20 + checksum: fe9ac61ec17596aed093bd8ae333537962f9256a4a6c7f018a7f628a69f8cd22 version: '1.0' diff --git a/packages/contentstack-auth/package.json b/packages/contentstack-auth/package.json index bc45d4a1b8..e3df89d46e 100644 --- a/packages/contentstack-auth/package.json +++ b/packages/contentstack-auth/package.json @@ -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": { @@ -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" diff --git a/packages/contentstack-command/package.json b/packages/contentstack-command/package.json index 50be27520b..1c30a4190d 100644 --- a/packages/contentstack-command/package.json +++ b/packages/contentstack-command/package.json @@ -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", @@ -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" diff --git a/packages/contentstack-config/package.json b/packages/contentstack-config/package.json index 3af3bc3756..75b6f0d9f3 100644 --- a/packages/contentstack-config/package.json +++ b/packages/contentstack-config/package.json @@ -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", @@ -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", diff --git a/packages/contentstack-utilities/package.json b/packages/contentstack-utilities/package.json index bc14071797..eb0aaba6c1 100644 --- a/packages/contentstack-utilities/package.json +++ b/packages/contentstack-utilities/package.json @@ -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", @@ -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", diff --git a/packages/contentstack-utilities/src/auth-handler.ts b/packages/contentstack-utilities/src/auth-handler.ts index 2b419defbb..1b6ddaea5e 100644 --- a/packages/contentstack-utilities/src/auth-handler.ts +++ b/packages/contentstack-utilities/src/auth-handler.ts @@ -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 | null = null; private cmaHost: string; set host(contentStackHost) { @@ -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(); diff --git a/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts b/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts index 751adc7fff..bf0d843e74 100644 --- a/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts +++ b/packages/contentstack-utilities/src/progress-summary/cli-progress-manager.ts @@ -103,10 +103,6 @@ export default class CLIProgressManager { return; } - if (configHandler.get('log')?.showConsoleLogs) { - return; - } - // Apply strategy-based corrections before printing CLIProgressManager.applyStrategyCorrections(); @@ -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; @@ -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})`, diff --git a/packages/contentstack-utilities/test/unit/auth-handler.test.ts b/packages/contentstack-utilities/test/unit/auth-handler.test.ts index faa22b37f7..d7f3b0d7d1 100644 --- a/packages/contentstack-utilities/test/unit/auth-handler.test.ts +++ b/packages/contentstack-utilities/test/unit/auth-handler.test.ts @@ -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(); @@ -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); }); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7186f6b1ad..7ca97ffb98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,10 +199,10 @@ importers: packages/contentstack-auth: dependencies: '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 + specifier: ~2.0.0-beta.5 version: link:../contentstack-command '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 + specifier: ~2.0.0-beta.5 version: link:../contentstack-utilities '@oclif/core': specifier: ^4.3.0 @@ -272,7 +272,7 @@ importers: packages/contentstack-command: dependencies: '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 + specifier: ~2.0.0-beta.5 version: link:../contentstack-utilities '@oclif/core': specifier: ^4.3.0 @@ -321,10 +321,10 @@ importers: packages/contentstack-config: dependencies: '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 + specifier: ~2.0.0-beta.5 version: link:../contentstack-command '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 + specifier: ~2.0.0-beta.5 version: link:../contentstack-utilities '@contentstack/utils': specifier: ~1.7.0 @@ -422,8 +422,8 @@ importers: packages/contentstack-utilities: dependencies: '@contentstack/management': - specifier: ~1.27.6 - version: 1.27.6(debug@4.4.3) + specifier: ~1.29.1 + version: 1.29.1 '@contentstack/marketplace-sdk': specifier: ^1.5.0 version: 1.5.0(debug@4.4.3) @@ -908,12 +908,19 @@ packages: resolution: {integrity: sha512-92h8YzKZ2EDzMogf0fmBHapCjVpzHkDBIj0Eb/MhPFIhlybDlAZhcM/di6zwgicEJj5UjTJ+ETXXQMEJZouDew==} engines: {node: '>=8.0.0'} + '@contentstack/management@1.29.1': + resolution: {integrity: sha512-TFzimKEcqLCXxh5GH9QnNCV0Ta0PrsSWMmXtshQYGw7atbtKpQNHhoZqO4ifVoMFlSnSe21MQrsJUoVbigSOSA==} + engines: {node: '>=8.0.0'} + '@contentstack/marketplace-sdk@1.5.0': resolution: {integrity: sha512-n2USMwswXBDtmVOg0t5FUks8X0d49u0UDFSrwxti09X/SONeP0P8wSqIDCjoB2gGRQc6fg/Fg2YPRvejUWeR4A==} '@contentstack/utils@1.7.1': resolution: {integrity: sha512-b/0t1malpJeFCNd9+1uN3BuO8mRn2b5+aNtrYEZ6YlSNjYNRu9IjqSxZ5Clhs5267950UV1ayhgFE8z3qre2eQ==} + '@contentstack/utils@1.8.0': + resolution: {integrity: sha512-pqCFbn2dynSCW6LUD2AH74LIy32dxxe52OL+HpUxNVXV5doFyClkFjP9toqdAZ81VbCEaOc4WK+VS/RdtMpxDA==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -6816,6 +6823,21 @@ snapshots: transitivePeerDependencies: - debug + '@contentstack/management@1.29.1': + dependencies: + '@contentstack/utils': 1.8.0 + assert: 2.1.0 + axios: 1.13.6(debug@4.4.3) + buffer: 6.0.3 + form-data: 4.0.5 + husky: 9.1.7 + lodash: 4.17.23 + otplib: 12.0.1 + qs: 6.15.0 + stream-browserify: 3.0.0 + transitivePeerDependencies: + - debug + '@contentstack/marketplace-sdk@1.5.0(debug@4.4.3)': dependencies: '@contentstack/utils': 1.7.1 @@ -6825,6 +6847,8 @@ snapshots: '@contentstack/utils@1.7.1': {} + '@contentstack/utils@1.8.0': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -9529,7 +9553,7 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@4.9.5) eslint-config-xo-space: 0.35.0(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 15.7.0(eslint@8.57.1) eslint-plugin-perfectionist: 2.11.0(eslint@8.57.1)(typescript@4.9.5) @@ -9586,7 +9610,7 @@ snapshots: eslint-config-xo: 0.49.0(eslint@8.57.1) eslint-config-xo-space: 0.35.0(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsdoc: 50.8.0(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 17.24.0(eslint@8.57.1)(typescript@4.9.5) @@ -9664,7 +9688,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -9740,7 +9764,7 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -9798,7 +9822,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9