From 00388af32a7e88a6050a55b40aeada8b5e39014f Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 16:50:23 +0530 Subject: [PATCH 1/3] added org plan check (cherry picked from commit 41b73639afd0f62f7c27845bc9dbebaa678cafd9) --- .talismanrc | 4 + .../src/commands/config/set/region.ts | 18 +++ .../src/interfaces/index.ts | 2 + .../src/utils/region-handler.ts | 4 + .../src/feature-status/build-auth-headers.ts | 43 +++++++ .../feature-status/feature-status-handler.ts | 49 ++++++++ .../src/feature-status/feature-uids.ts | 6 + .../src/feature-status/index.ts | 5 + .../src/feature-status/resolve-auth-host.ts | 13 +++ .../src/feature-status/types.ts | 16 +++ packages/contentstack-utilities/src/index.ts | 2 + .../src/hooks/prerun/plan-guard.ts | 110 ++++++++++++++++++ .../contentstack/src/utils/context-handler.ts | 4 +- 13 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 packages/contentstack-utilities/src/feature-status/build-auth-headers.ts create mode 100644 packages/contentstack-utilities/src/feature-status/feature-status-handler.ts create mode 100644 packages/contentstack-utilities/src/feature-status/feature-uids.ts create mode 100644 packages/contentstack-utilities/src/feature-status/index.ts create mode 100644 packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts create mode 100644 packages/contentstack-utilities/src/feature-status/types.ts create mode 100644 packages/contentstack/src/hooks/prerun/plan-guard.ts diff --git a/.talismanrc b/.talismanrc index bedb52350d..169549a7cb 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,4 +1,8 @@ fileignoreconfig: - filename: pnpm-lock.yaml checksum: ba838353c1277083cb35b5f69cbbe00f0dc5d7d69e49b44c71c3a7a79e95ee73 + - filename: packages/contentstack-utilities/src/feature-status/build-auth-headers.ts + checksum: 9360dfbce41aa9c8a62889f4821c37fc222b04b8b1c552d1dfbcd8f2854d49e0 + - filename: packages/contentstack/src/hooks/prerun/plan-guard.ts + checksum: 74eeb5c98e9ae48bf6f2918e75a7cfca23915396ffdd177b8716acba35c287da version: '1.0' diff --git a/packages/contentstack-config/src/commands/config/set/region.ts b/packages/contentstack-config/src/commands/config/set/region.ts index edc1311bac..a0b8860813 100644 --- a/packages/contentstack-config/src/commands/config/set/region.ts +++ b/packages/contentstack-config/src/commands/config/set/region.ts @@ -51,6 +51,12 @@ export default class RegionSetCommand extends BaseCommand { + const host = resolveAuthHost(ctx); + const headers = buildAuthHeaders(ctx); + + const client = new HttpClient(); + client.baseUrl(host).headers(headers); + + const res = await client.get( + `/v1/feature-status?feature_uid=${encodeURIComponent(featureUid)}`, + ); + + if (res.status < 200 || res.status >= 300) { + throw new Error(`PLAN_CHECK: feature-status API returned ${res.status} for "${featureUid}".`); + } + + return res.data as FeatureStatus; +} + +export async function assertFeatureEnabled(featureUid: string, ctx?: FeatureCtx): Promise { + let status: FeatureStatus; + try { + status = await isFeatureEnabled(featureUid, ctx); + } catch (e) { + throw new Error( + `Could not verify your plan for "${featureUid}". ` + + `This command requires a confirmed plan status to run. ` + + `Please retry; if the problem persists contact support. (${(e as Error).message})`, + ); + } + + if (!status.is_part_of_plan) { + throw new Error( + `"${featureUid}" is not part of your current plan. Please upgrade your plan to use this feature.`, + ); + } + + if (status.status !== 'enabled') { + throw new Error( + `"${featureUid}" is not enabled for your plan (status: ${status.status}).`, + ); + } + + return status; +} diff --git a/packages/contentstack-utilities/src/feature-status/feature-uids.ts b/packages/contentstack-utilities/src/feature-status/feature-uids.ts new file mode 100644 index 0000000000..d333ffb81f --- /dev/null +++ b/packages/contentstack-utilities/src/feature-status/feature-uids.ts @@ -0,0 +1,6 @@ +export const FEATURE = { + ASSET_MANAGEMENT: 'amAssets', + // ASSET_SCANNING: 'asset_scanning', // uncomment when uid confirmed with platform team +} as const; + +export type FeatureUid = typeof FEATURE[keyof typeof FEATURE]; diff --git a/packages/contentstack-utilities/src/feature-status/index.ts b/packages/contentstack-utilities/src/feature-status/index.ts new file mode 100644 index 0000000000..97531ef9a3 --- /dev/null +++ b/packages/contentstack-utilities/src/feature-status/index.ts @@ -0,0 +1,5 @@ +export * from './types'; +export * from './feature-uids'; +export { resolveAuthHost } from './resolve-auth-host'; +export { buildAuthHeaders } from './build-auth-headers'; +export { isFeatureEnabled, assertFeatureEnabled } from './feature-status-handler'; diff --git a/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts b/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts new file mode 100644 index 0000000000..a86e4786ee --- /dev/null +++ b/packages/contentstack-utilities/src/feature-status/resolve-auth-host.ts @@ -0,0 +1,13 @@ +import configHandler from '../config-handler'; + +export function resolveAuthHost(ctx?: { region?: { authUrl?: string } }): string { + const region = (ctx?.region ?? configHandler.get('region')) as { authUrl?: string } | undefined; + const authUrl = region?.authUrl; + if (!authUrl) { + throw new Error( + 'PLAN_CHECK: Auth host is not configured for the current region. ' + + "Re-run `csdx config:set:region` to refresh region endpoints.", + ); + } + return String(authUrl).replace(/\/$/, ''); +} diff --git a/packages/contentstack-utilities/src/feature-status/types.ts b/packages/contentstack-utilities/src/feature-status/types.ts new file mode 100644 index 0000000000..136c3fee66 --- /dev/null +++ b/packages/contentstack-utilities/src/feature-status/types.ts @@ -0,0 +1,16 @@ +export interface FeatureStatus { + org_uid: string; + feature_key: string; + status: 'enabled' | 'disabled' | string; + limit: number; + max_limit: number; + is_part_of_plan: boolean; +} + +export interface FeatureCtx { + apiKey?: string; + orgUid?: string; + managementToken?: string; + authToken?: string; + region?: { authUrl?: string; name?: string; cma?: string }; +} diff --git a/packages/contentstack-utilities/src/index.ts b/packages/contentstack-utilities/src/index.ts index db62ccc212..66ed0e16a3 100644 --- a/packages/contentstack-utilities/src/index.ts +++ b/packages/contentstack-utilities/src/index.ts @@ -80,3 +80,5 @@ export { default as TablePrompt } from './inquirer-table-prompt'; export { Logger }; export { default as authenticationHandler } from './authentication-handler'; export {v2Logger as log, cliErrorHandler, handleAndLogError, getLogPath} from './logger/log' + +export * from './feature-status'; diff --git a/packages/contentstack/src/hooks/prerun/plan-guard.ts b/packages/contentstack/src/hooks/prerun/plan-guard.ts new file mode 100644 index 0000000000..fd835d22b6 --- /dev/null +++ b/packages/contentstack/src/hooks/prerun/plan-guard.ts @@ -0,0 +1,110 @@ +import { + configHandler, + assertFeatureEnabled, + FeatureCtx, + FeatureStatus, + log, + handleAndLogError, + cliux, +} from '@contentstack/cli-utilities'; + +function getArgvFlag(argv: string[], ...names: string[]): string | undefined { + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + for (const name of names) { + if (arg === name && argv[i + 1] && !argv[i + 1].startsWith('-')) { + return argv[i + 1]; + } + if (arg.startsWith(`${name}=`)) { + return arg.split('=').slice(1).join('='); + } + } + } + return undefined; +} + +export default async function (opts: { Command?: { id?: string }; argv?: string[]; config?: any }): Promise { + const config = opts?.config ?? this.config; + const commandId = opts?.Command?.id; + const argv: string[] = opts?.argv ?? []; + + if (!commandId) return; + + const planProtectedCommands: Record = + config?.context?.plugin?.config?.planProtectedCommands ?? {}; + + const requiredFeatures: string[] = planProtectedCommands[commandId]; + if (!requiredFeatures || requiredFeatures.length === 0) return; + + // Resolve alias → management token + const aliasFromArgv = getArgvFlag(argv, '--alias', '-a'); + + // Clone uses different flag names — handle both alias forms + const cloneSourceAlias = + commandId === 'cm:stacks:clone' + ? getArgvFlag(argv, '--source-management-token-alias') ?? + getArgvFlag(argv, '--destination-management-token-alias') + : undefined; + + const alias = aliasFromArgv ?? cloneSourceAlias; + let managementToken: string | undefined; + let apiKeyFromAlias: string | undefined; + if (alias) { + const stored = configHandler.get(`tokens.${alias}`) as + | { token?: string; apiKey?: string; type?: string } + | undefined; + if (stored?.token && stored?.apiKey && stored?.type !== 'delivery') { + managementToken = stored.token; + apiKeyFromAlias = stored.apiKey; + } + } + + const authorisationType = configHandler.get('authorisationType') as string | undefined; + const oauthToken = configHandler.get('oauthAccessToken') as string | undefined; + const isOAuth = authorisationType === 'OAUTH' && !!oauthToken; + + const authtoken = configHandler.get('authtoken') as string | undefined; + const isBasic = authorisationType === 'BASIC' && !!authtoken; + + const apiKeyFromArgv = getArgvFlag(argv, '--stack-api-key', '-k'); + const orgUid = configHandler.get('oauthOrgUid') as string | undefined; + + const canCheckNow = + (!!managementToken && !!apiKeyFromAlias) || + isOAuth || + (isBasic && !!(apiKeyFromArgv || orgUid)); + + if (!canCheckNow) { + config.context.planCheckRequired = requiredFeatures; + log.debug( + `[plan-guard] Deferred plan check for: ${requiredFeatures.join(', ')} — credentials not resolvable at prerun`, + { module: 'plan-guard', commandId }, + ); + return; + } + + const region = configHandler.get('region') as { authUrl?: string; name?: string } | undefined; + const ctx: FeatureCtx = { + managementToken, + apiKey: apiKeyFromAlias ?? apiKeyFromArgv, + authToken: authtoken, + orgUid, + region, + }; + + const planStatus: Record = {}; + for (const featureUid of requiredFeatures) { + try { + const result = await assertFeatureEnabled(featureUid, ctx); + planStatus[featureUid] = result; + log.debug(`[plan-guard] Feature "${featureUid}" is enabled.`, { module: 'plan-guard', commandId }); + } catch (error) { + handleAndLogError(error, { module: 'plan-guard', commandId, featureUid }); + cliux.error((error as Error).message); + this.exit(1); + return; + } + } + + config.context.planStatus = planStatus; +} diff --git a/packages/contentstack/src/utils/context-handler.ts b/packages/contentstack/src/utils/context-handler.ts index bc4a0da678..f95798b731 100644 --- a/packages/contentstack/src/utils/context-handler.ts +++ b/packages/contentstack/src/utils/context-handler.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { configHandler, pathValidator, sanitizePath, generateShortUid } from '@contentstack/cli-utilities'; +import { configHandler, pathValidator, sanitizePath, generateShortUid, FeatureStatus } from '@contentstack/cli-utilities'; import { machineIdSync } from 'node-machine-id'; export default class CsdxContext { @@ -16,6 +16,8 @@ export default class CsdxContext { public flagWarningPrintState: any; public flags: any; public cliVersion: string; + public planStatus: Record = {}; + public planCheckRequired: string[] = []; constructor(cliOpts: any, cliConfig: any) { const analyticsInfo = []; From 927d385d2e2060ba5f9e6c7d43d9b941672004e1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 1 Jul 2026 11:21:03 +0530 Subject: [PATCH 2/3] fixed org plan check (cherry picked from commit 6db9d0d1547fc2fcb5481f8da116235265830437) --- packages/contentstack-command/src/index.ts | 2 +- .../src/feature-status/feature-uids.ts | 4 +- .../src/hooks/init/context-init.ts | 7 ++- .../src/hooks/prerun/plan-guard.ts | 56 ++++++------------- packages/contentstack/tsconfig.json | 2 +- 5 files changed, 28 insertions(+), 43 deletions(-) diff --git a/packages/contentstack-command/src/index.ts b/packages/contentstack-command/src/index.ts index 35d6cc6181..afc22781ad 100644 --- a/packages/contentstack-command/src/index.ts +++ b/packages/contentstack-command/src/index.ts @@ -14,7 +14,7 @@ abstract class ContentstackCommand extends Command { get context() { // @ts-ignore - return this.config.context || {}; + return this.config.context || this.config.options?.context || {}; } get email() { diff --git a/packages/contentstack-utilities/src/feature-status/feature-uids.ts b/packages/contentstack-utilities/src/feature-status/feature-uids.ts index d333ffb81f..fc55046e22 100644 --- a/packages/contentstack-utilities/src/feature-status/feature-uids.ts +++ b/packages/contentstack-utilities/src/feature-status/feature-uids.ts @@ -1,6 +1,6 @@ export const FEATURE = { ASSET_MANAGEMENT: 'amAssets', - // ASSET_SCANNING: 'asset_scanning', // uncomment when uid confirmed with platform team + ASSET_SCANNING: 'assetsScan', } as const; -export type FeatureUid = typeof FEATURE[keyof typeof FEATURE]; +export type FeatureUid = (typeof FEATURE)[keyof typeof FEATURE]; diff --git a/packages/contentstack/src/hooks/init/context-init.ts b/packages/contentstack/src/hooks/init/context-init.ts index 4d87969130..8b72ee68cb 100644 --- a/packages/contentstack/src/hooks/init/context-init.ts +++ b/packages/contentstack/src/hooks/init/context-init.ts @@ -9,5 +9,10 @@ export default function (opts): void { if (opts.id) { configHandler.set('currentCommandId', opts.id); } - this.config.context = new CsdxContext(opts, this.config); + const ctx = new CsdxContext(opts, this.config); + this.config.context = ctx; + // oclif v4 always recreates Config via Config.load(existingConfig), stripping custom + // properties like `context`. Storing it in options ensures it survives the spread: + // new Config({ ...opts.options, plugins }) in Config.load + (this.config.options as any).context = ctx; } diff --git a/packages/contentstack/src/hooks/prerun/plan-guard.ts b/packages/contentstack/src/hooks/prerun/plan-guard.ts index fd835d22b6..1ab99a10d8 100644 --- a/packages/contentstack/src/hooks/prerun/plan-guard.ts +++ b/packages/contentstack/src/hooks/prerun/plan-guard.ts @@ -1,12 +1,4 @@ -import { - configHandler, - assertFeatureEnabled, - FeatureCtx, - FeatureStatus, - log, - handleAndLogError, - cliux, -} from '@contentstack/cli-utilities'; +import { configHandler, isFeatureEnabled, FeatureCtx, FeatureStatus, log } from '@contentstack/cli-utilities'; function getArgvFlag(argv: string[], ...names: string[]): string | undefined { for (let i = 0; i < argv.length; i++) { @@ -23,30 +15,17 @@ function getArgvFlag(argv: string[], ...names: string[]): string | undefined { return undefined; } -export default async function (opts: { Command?: { id?: string }; argv?: string[]; config?: any }): Promise { +export default async function (opts: { Command?: { id?: string; planProtectedFeatures?: string[] }; argv?: string[]; config?: any }): Promise { const config = opts?.config ?? this.config; const commandId = opts?.Command?.id; const argv: string[] = opts?.argv ?? []; if (!commandId) return; - const planProtectedCommands: Record = - config?.context?.plugin?.config?.planProtectedCommands ?? {}; + const requiredFeatures: string[] = opts?.Command?.planProtectedFeatures ?? []; + if (requiredFeatures.length === 0) return; - const requiredFeatures: string[] = planProtectedCommands[commandId]; - if (!requiredFeatures || requiredFeatures.length === 0) return; - - // Resolve alias → management token - const aliasFromArgv = getArgvFlag(argv, '--alias', '-a'); - - // Clone uses different flag names — handle both alias forms - const cloneSourceAlias = - commandId === 'cm:stacks:clone' - ? getArgvFlag(argv, '--source-management-token-alias') ?? - getArgvFlag(argv, '--destination-management-token-alias') - : undefined; - - const alias = aliasFromArgv ?? cloneSourceAlias; + const alias = getArgvFlag(argv, '--alias', '-a'); let managementToken: string | undefined; let apiKeyFromAlias: string | undefined; if (alias) { @@ -69,10 +48,7 @@ export default async function (opts: { Command?: { id?: string }; argv?: string[ const apiKeyFromArgv = getArgvFlag(argv, '--stack-api-key', '-k'); const orgUid = configHandler.get('oauthOrgUid') as string | undefined; - const canCheckNow = - (!!managementToken && !!apiKeyFromAlias) || - isOAuth || - (isBasic && !!(apiKeyFromArgv || orgUid)); + const canCheckNow = (!!managementToken && !!apiKeyFromAlias) || isOAuth || (isBasic && !!(apiKeyFromArgv || orgUid)); if (!canCheckNow) { config.context.planCheckRequired = requiredFeatures; @@ -93,18 +69,22 @@ export default async function (opts: { Command?: { id?: string }; argv?: string[ }; const planStatus: Record = {}; + const failedFeatures: string[] = []; for (const featureUid of requiredFeatures) { try { - const result = await assertFeatureEnabled(featureUid, ctx); - planStatus[featureUid] = result; - log.debug(`[plan-guard] Feature "${featureUid}" is enabled.`, { module: 'plan-guard', commandId }); + planStatus[featureUid] = await isFeatureEnabled(featureUid, ctx); + log.debug(`[plan-guard] Feature "${featureUid}" status fetched.`, { module: 'plan-guard', commandId }); } catch (error) { - handleAndLogError(error, { module: 'plan-guard', commandId, featureUid }); - cliux.error((error as Error).message); - this.exit(1); - return; + log.warn(`[plan-guard] Could not fetch status for "${featureUid}": ${(error as Error).message}`, { + module: 'plan-guard', + commandId, + featureUid, + }); + failedFeatures.push(featureUid); } } - config.context.planStatus = planStatus; + if (failedFeatures.length > 0) { + config.context.planCheckRequired = failedFeatures; + } } diff --git a/packages/contentstack/tsconfig.json b/packages/contentstack/tsconfig.json index c12161b0f1..be6eadc8e2 100644 --- a/packages/contentstack/tsconfig.json +++ b/packages/contentstack/tsconfig.json @@ -8,7 +8,7 @@ "strict": false, "target": "es2017", "allowJs": true, - "sourceMap": false, + "sourceMap": true, "skipLibCheck": true, "esModuleInterop": true, "lib": [ From d833aa7876bc6160d2de0628011384099efcb319 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 20 Jul 2026 12:46:25 +0530 Subject: [PATCH 3/3] removed invalid urls set in region --- .talismanrc | 2 ++ .../src/commands/config/set/region.ts | 9 --------- .../src/interfaces/index.ts | 3 +-- .../src/utils/region-handler.ts | 19 +++---------------- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/.talismanrc b/.talismanrc index 169549a7cb..eae2f228bf 100644 --- a/.talismanrc +++ b/.talismanrc @@ -5,4 +5,6 @@ fileignoreconfig: checksum: 9360dfbce41aa9c8a62889f4821c37fc222b04b8b1c552d1dfbcd8f2854d49e0 - filename: packages/contentstack/src/hooks/prerun/plan-guard.ts checksum: 74eeb5c98e9ae48bf6f2918e75a7cfca23915396ffdd177b8716acba35c287da + - filename: packages/contentstack-config/src/utils/region-handler.ts + checksum: fe0713019d71cad642125de835a15a20784c961d6c83a9271f7287e73c1d4141 version: '1.0' diff --git a/packages/contentstack-config/src/commands/config/set/region.ts b/packages/contentstack-config/src/commands/config/set/region.ts index a0b8860813..0a640927f5 100644 --- a/packages/contentstack-config/src/commands/config/set/region.ts +++ b/packages/contentstack-config/src/commands/config/set/region.ts @@ -51,9 +51,6 @@ export default class RegionSetCommand extends BaseCommand