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: 6 additions & 2 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
fileignoreconfig:
- filename: pnpm-lock.yaml
checksum: a72a1c8e7109e8eb3a4159b7945e20cdb25a99bff32e45fcbf5a6b4534bdb0da
checksum: dd5f6ad633313054c21fa0e24645a25dca360f12f21b877472f514d848340ac6
- filename: packages/contentstack-utilities/src/message-handler.ts
checksum: e7221e8413005b9efe3a230cd91aff130850976addc41c0acb10745a56ec3245
version: '1.0'
- filename: packages/contentstack-auth/src/commands/auth/tokens/list.ts
checksum: cff2456db5a6eb9c056096e5b08d4737ad4507386f9998f9182d3acab8be868d
- filename: packages/contentstack/README.md
checksum: 4aec6271d09ee38452460140b1d8da338df15c6ed765544246c00417d179dba5
version: '1.0'
2 changes: 1 addition & 1 deletion packages/contentstack-command/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ export default class RegionSetCommand extends BaseCommand<typeof RegionSetComman
'cs-assets': _flags.string({
description: 'Custom host to set for Contentstack Assets API',
}),
'auth-api': _flags.string({
description: 'Custom host to set for Auth API',
}),
};
static examples = [
'$ csdx config:set:region',
Expand Down Expand Up @@ -82,6 +85,7 @@ export default class RegionSetCommand extends BaseCommand<typeof RegionSetComman
let launchHubUrl = regionSetFlags['launch'];
let composableStudioUrl = regionSetFlags['studio'];
let csAssetsUrl = regionSetFlags['cs-assets'];
let authUrl = regionSetFlags['auth-api'];
let selectedRegion = args.region;
if (!(cda && cma && uiHost && name) && !selectedRegion) {
selectedRegion = await interactive.askRegions();
Expand Down Expand Up @@ -115,6 +119,9 @@ export default class RegionSetCommand extends BaseCommand<typeof RegionSetComman
if (!csAssetsUrl) {
csAssetsUrl = this.transformUrl(cma, 'am-api');
}
if (!authUrl) {
authUrl = this.transformUrl(cma, 'auth-api');
}
let customRegion: Region = {
cda,
cma,
Expand All @@ -125,6 +132,7 @@ export default class RegionSetCommand extends BaseCommand<typeof RegionSetComman
launchHubUrl,
composableStudioUrl,
csAssetsUrl,
authUrl,
};
customRegion = regionHandler.setCustomRegion(customRegion);
await authHandler.setConfigData('logout'); //Todo: Handle this logout flow well through logout command call
Expand All @@ -137,6 +145,7 @@ export default class RegionSetCommand extends BaseCommand<typeof RegionSetComman
cliux.success(`Launch URL: ${customRegion.launchHubUrl}`);
cliux.success(`Studio URL: ${customRegion.composableStudioUrl}`);
cliux.success(`Contentstack Assets URL: ${customRegion.csAssetsUrl}`);
cliux.success(`Auth API URL: ${customRegion.authUrl}`);
} catch (error) {
handleAndLogError(error, { ...this.contextDetails, module: 'config-set-region' });
}
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-config/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface Region {
launchHubUrl: string;
composableStudioUrl: string;
csAssetsUrl?: string;
authUrl?: string;
}

export interface Limit {
Expand Down
2 changes: 2 additions & 0 deletions packages/contentstack-config/src/utils/region-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function getRegionObject(regionKey: string): Region {
personalizeUrl: endpoints.personalizeManagement,
composableStudioUrl: endpoints.composableStudio,
csAssetsUrl: endpoints.assetManagement,
authUrl: endpoints.auth,
};
} catch {
return null;
Expand Down Expand Up @@ -157,6 +158,7 @@ class UserConfig {
launchHubUrl: regionObject['launchHubUrl'],
composableStudioUrl: regionObject['composableStudioUrl'],
csAssetsUrl: regionObject['csAssetsUrl'],
authUrl: regionObject['authUrl'],
};

return sanitizedRegion;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import configHandler from '../config-handler';

export interface AuthHeaders {
[key: string]: string;
}

export function buildAuthHeaders(ctx?: {
managementToken?: string;
apiKey?: string;
authToken?: string;
orgUid?: string;
}): AuthHeaders {
// 1. Management token takes priority (no 'Bearer' prefix — API contract)
if (ctx?.managementToken && ctx?.apiKey) {
return {
Authorization: ctx.managementToken,
api_key: ctx.apiKey,
};
}

// 2. OAuth
const oauthToken = configHandler.get('oauthAccessToken') as string | undefined;
if (oauthToken) {
return { Authorization: `Bearer ${oauthToken}` };
}

// 3. Authtoken + organization_uid
const authtoken = ctx?.authToken ?? (configHandler.get('authtoken') as string | undefined);
const orgUid = ctx?.orgUid ?? (configHandler.get('oauthOrgUid') as string | undefined);
if (authtoken && orgUid) {
return { authtoken, organization_uid: orgUid };
}

// 4. Authtoken + api_key
if (authtoken && ctx?.apiKey) {
return { authtoken, api_key: ctx.apiKey };
}

throw new Error(
'PLAN_CHECK: Cannot build auth headers — no valid credentials available. ' +
'Please log in or provide a management token alias.',
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { HttpClient } from '../http-client';
import { resolveAuthHost } from './resolve-auth-host';
import { buildAuthHeaders } from './build-auth-headers';
import { FeatureStatus, FeatureCtx } from './types';

export async function isFeatureEnabled(featureUid: string, ctx?: FeatureCtx): Promise<FeatureStatus> {
const host = resolveAuthHost(ctx);
const headers = buildAuthHeaders(ctx);

const client = new HttpClient();
client.baseUrl(host).headers(headers);

const res = await client.get<FeatureStatus>(
`/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<FeatureStatus> {
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const FEATURE = {
ASSET_MANAGEMENT: 'amAssets',
ASSET_SCANNING: 'assetsScan',
} as const;

export type FeatureUid = (typeof FEATURE)[keyof typeof FEATURE];
5 changes: 5 additions & 0 deletions packages/contentstack-utilities/src/feature-status/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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(/\/$/, '');
}
16 changes: 16 additions & 0 deletions packages/contentstack-utilities/src/feature-status/types.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
4 changes: 3 additions & 1 deletion packages/contentstack-utilities/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,6 @@ export {
ProgressStrategyRegistry,
CustomProgressStrategy,
DefaultProgressStrategy
} from './progress-summary';
} from './progress-summary';

export * from './feature-status';
3 changes: 2 additions & 1 deletion packages/contentstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"hooks": {
"prerun": [
"./lib/hooks/prerun/init-context-for-command",
"./lib/hooks/prerun/plan-guard",
"./lib/hooks/prerun/default-rate-limit-check",
"./lib/hooks/prerun/latest-version-warning"
],
Expand All @@ -170,4 +171,4 @@
"url": "git+https://github.com/contentstack/cli.git",
"directory": "packages/contentstack"
}
}
}
7 changes: 6 additions & 1 deletion packages/contentstack/src/hooks/init/context-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
90 changes: 90 additions & 0 deletions packages/contentstack/src/hooks/prerun/plan-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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++) {
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<void> {
const config = opts?.config ?? this.config;
const commandId = opts?.Command?.id;
const argv: string[] = opts?.argv ?? [];

if (!commandId) return;

const requiredFeatures: string[] = config?.context?.plugin?.config?.planProtectedFeatures ?? [];
if (requiredFeatures.length === 0) return;

const alias = getArgvFlag(argv, '--alias', '-a');
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<string, FeatureStatus> = {};
const failedFeatures: string[] = [];
for (const featureUid of requiredFeatures) {
try {
planStatus[featureUid] = await isFeatureEnabled(featureUid, ctx);
log.debug(`[plan-guard] Feature "${featureUid}" status fetched.`, { module: 'plan-guard', commandId });
} catch (error) {
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;
}
}
4 changes: 3 additions & 1 deletion packages/contentstack/src/utils/context-handler.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,6 +16,8 @@ export default class CsdxContext {
public flagWarningPrintState: any;
public flags: any;
public cliVersion: string;
public planStatus: Record<string, FeatureStatus> = {};
public planCheckRequired: string[] = [];

constructor(cliOpts: any, cliConfig: any) {
const analyticsInfo = [];
Expand Down
2 changes: 1 addition & 1 deletion packages/contentstack/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"strict": false,
"target": "es2017",
"allowJs": true,
"sourceMap": false,
"sourceMap": true,
"skipLibCheck": true,
"esModuleInterop": true,
"lib": [
Expand Down
Loading
Loading