Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion .github/workflows/sca-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
args: --all-projects --fail-on=all
json: true
continue-on-error: true
- uses: contentstack/sca-policy@main
- uses: contentstack/sca-policy@main
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": "1.8.0-beta.0",
"version": "1.8.0-beta.1",
"author": "Contentstack",
"bugs": "https://github.com/contentstack/cli/issues",
"scripts": {
Expand All @@ -22,8 +22,8 @@
"test:unit:report": "nyc --extension .ts mocha --forbid-only \"test/unit/**/*.test.ts\""
},
"dependencies": {
"@contentstack/cli-command": "~1.8.0-beta.0",
"@contentstack/cli-utilities": "~1.18.0-beta.0",
"@contentstack/cli-command": "~1.8.0-beta.1",
"@contentstack/cli-utilities": "~1.19.0-beta.0",
"@oclif/core": "^4.3.0",
"@oclif/plugin-help": "^6.2.28",
"otplib": "^12.0.1"
Expand Down Expand Up @@ -81,4 +81,4 @@
}
},
"repository": "contentstack/cli"
}
}
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": "1.8.0-beta.0",
"version": "1.8.0-beta.1",
"author": "Contentstack",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand All @@ -20,7 +20,7 @@
"test:unit": "mocha --timeout 10000 --forbid-only \"test/unit/**/*.test.ts\""
},
"dependencies": {
"@contentstack/cli-utilities": "~1.18.0-beta.0",
"@contentstack/cli-utilities": "~1.19.0-beta.0",
"contentstack": "^3.25.3",
"@oclif/core": "^4.3.0",
"@oclif/plugin-help": "^6.2.28"
Expand Down Expand Up @@ -65,4 +65,4 @@
"repositoryPrefix": "<%- repo %>/blob/main/packages/contentstack-command/<%- commandPath %>"
},
"repository": "contentstack/cli"
}
}
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": "1.20.0-beta.0",
"version": "1.20.0-beta.1",
"author": "Contentstack",
"scripts": {
"build": "pnpm compile && oclif manifest && oclif readme",
Expand All @@ -21,8 +21,8 @@
"test:unit:report": "nyc --extension .ts mocha --forbid-only \"test/unit/**/*.test.ts\""
},
"dependencies": {
"@contentstack/cli-command": "~1.8.0-beta.0",
"@contentstack/cli-utilities": "~1.18.0-beta.0",
"@contentstack/cli-command": "~1.8.0-beta.1",
"@contentstack/cli-utilities": "~1.19.0-beta.0",
"@contentstack/utils": "~1.7.0",
"@oclif/core": "^4.3.0",
"@oclif/plugin-help": "^6.2.28",
Expand Down Expand Up @@ -95,4 +95,4 @@
}
},
"repository": "contentstack/cli"
}
}
4 changes: 2 additions & 2 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": "1.18.0-beta.0",
"version": "1.19.0-beta.0",
"description": "Utilities for contentstack projects",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down Expand Up @@ -82,4 +82,4 @@
"ts-node": "^10.9.2",
"typescript": "^4.9.5"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { client, ContentstackClient, ContentstackConfig } from '@contentstack/ma
import authHandler from './auth-handler';
import { Agent } from 'node:https';
import configHandler, { default as configStore } from './config-handler';
import { getProxyConfig } from './proxy-helper';
import { getProxyConfigForHost, resolveRequestHost, clearProxyEnv } from './proxy-helper';
import dotenv from 'dotenv';

dotenv.config();
Expand All @@ -17,8 +17,15 @@ class ManagementSDKInitiator {
}

async createAPIClient(config): Promise<ContentstackClient> {
// Get proxy configuration with priority: Environment variables > Global config
const proxyConfig = getProxyConfig();
// Resolve host so NO_PROXY applies even when config.host is omitted (e.g. from region.cma)
const host = resolveRequestHost(config);
// NO_PROXY has priority over HTTP_PROXY/HTTPS_PROXY and config-set proxy
const proxyConfig = getProxyConfigForHost(host);

// When bypassing, clear proxy env immediately so SDK never see it (they may read at init or first request).
if (!proxyConfig) {
clearProxyEnv();
}

const option: ContentstackConfig = {
host: config.host,
Expand Down
23 changes: 20 additions & 3 deletions packages/contentstack-utilities/src/http-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@ import { IHttpClient } from './client-interface';
import { HttpResponse } from './http-response';
import configStore from '../config-handler';
import authHandler from '../auth-handler';
import { hasProxy, getProxyUrl, getProxyConfig } from '../proxy-helper';
import { hasProxy, getProxyUrl, getProxyConfig, getProxyConfigForHost } from '../proxy-helper';

/**
* Derive request host from baseURL or url for NO_PROXY checks.
*/
function getRequestHost(baseURL?: string, url?: string): string | undefined {
const toTry = [baseURL, url].filter(Boolean) as string[];
for (const candidateUrl of toTry) {
try {
const parsed = new URL(candidateUrl.startsWith('http') ? candidateUrl : `https://${candidateUrl}`);
return parsed.hostname || undefined;
} catch {
// Invalid URL; try next candidate (baseURL or url)
}
}
return undefined;
}

export type HttpClientOptions = {
disableEarlyAccessHeaders?: boolean;
Expand Down Expand Up @@ -411,9 +427,10 @@ export class HttpClient implements IHttpClient {
}
}

// Configure proxy if available (priority: request.proxy > getProxyConfig())
// Configure proxy if available. NO_PROXY has priority: hosts in NO_PROXY never use proxy.
if (!this.request.proxy) {
const proxyConfig = getProxyConfig();
const host = getRequestHost(this.request.baseURL, url);
const proxyConfig = host ? getProxyConfigForHost(host) : getProxyConfig();
if (proxyConfig) {
this.request.proxy = proxyConfig;
}
Expand Down
136 changes: 133 additions & 3 deletions packages/contentstack-utilities/src/proxy-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,84 @@ export interface ProxyConfig {
}

/**
* Get proxy configuration with priority: Environment variables > Global config
* Parse NO_PROXY / no_proxy env (both uppercase and lowercase).
* NO_PROXY has priority over HTTP_PROXY/HTTPS_PROXY: hosts in this list never use the proxy.
* Values are hostnames only, comma-separated; leading dot matches subdomains (e.g. .contentstack.io).
* The bypass list is fully dynamic: only env values are used (no hardcoded default).
* @returns List of trimmed entries, or empty array when NO_PROXY/no_proxy is unset
*/
export function getNoProxyList(): string[] {
const raw = process.env.NO_PROXY || process.env.no_proxy || '';
return raw
.split(',')
.map((s) => s.trim())
.filter(Boolean);
}

/**
* Normalize host for NO_PROXY matching: strip protocol/URL, port, lowercase, handle IPv6 brackets.
* Accepts hostname, host:port, or full URL (e.g. https://api.contentstack.io).
*/
function normalizeHost(host: string): string {
if (!host || typeof host !== 'string') return '';
let h = host.trim().toLowerCase();
// If it looks like a URL, extract hostname so NO_PROXY matching works (e.g. region.cma is full URL)
if (h.includes('://')) {
try {
const u = new URL(h);
h = u.hostname;
} catch {
// fall through to port stripping below
}
}
const portIdx = h.lastIndexOf(':');
if (h.startsWith('[')) {
const close = h.indexOf(']');
if (close !== -1 && h.length > close + 1 && h[close + 1] === ':') {
h = h.slice(1, close);
}
} else if (portIdx !== -1) {
const after = h.slice(portIdx + 1);
if (/^\d+$/.test(after)) {
h = h.slice(0, portIdx);
}
}
return h;
}

/**
* Check if the given host should bypass the proxy based on NO_PROXY / no_proxy.
* Supports: exact host, leading-dot subdomain match (e.g. .contentstack.io), and wildcard *.
* @param host - Request hostname (with or without port; will be normalized)
* @returns true if proxy should not be used for this host
*/
export function shouldBypassProxy(host: string): boolean {
const normalized = normalizeHost(host);
if (!normalized) return false;

const list = getNoProxyList();
for (const entry of list) {
const e = entry.trim().toLowerCase();
if (!e) continue;
if (e === '*') return true;
if (e.startsWith('.')) {
const domain = e.slice(1);
if (normalized === domain || normalized.endsWith(e)) return true;
} else {
if (normalized === e) return true;
}
}
return false;
}

/**
* Get proxy configuration. Sources (in order): env (HTTP_PROXY/HTTPS_PROXY), then global config
* from `csdx config:set:proxy --host <host> --port <port> --protocol <protocol>`.
* For per-request use, prefer getProxyConfigForHost(host) so NO_PROXY overrides both sources.
* @returns ProxyConfig object or undefined if no proxy is configured
*/
export function getProxyConfig(): ProxyConfig | undefined {
// Priority 1: Check environment variables (HTTPS_PROXY or HTTP_PROXY)
// Priority 1: Environment variables (HTTPS_PROXY or HTTP_PROXY)
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;

if (proxyUrl) {
Expand Down Expand Up @@ -46,7 +119,7 @@ export function getProxyConfig(): ProxyConfig | undefined {
}
}

// Priority 2: Check global config store
// Priority 2: Global config (csdx config:set:proxy)
const globalProxyConfig = configStore.get('proxy');
if (globalProxyConfig) {
if (typeof globalProxyConfig === 'object') {
Expand Down Expand Up @@ -86,6 +159,63 @@ export function getProxyConfig(): ProxyConfig | undefined {
return undefined;
}

/**
* Get proxy config only when the request host is not in NO_PROXY.
* NO_PROXY has priority over both HTTP_PROXY/HTTPS_PROXY and over proxy set via
* `csdx config:set:proxy` — if the host matches NO_PROXY, no proxy is used.
* Use this for all outbound requests so Contentstack and localhost bypass the proxy when set.
* @param host - Request hostname (e.g. api.contentstack.io or full URL like https://api.contentstack.io)
* @returns ProxyConfig or undefined if proxy is disabled or host should bypass (NO_PROXY)
*/
export function getProxyConfigForHost(host: string): ProxyConfig | undefined {
if (shouldBypassProxy(host)) return undefined;
return getProxyConfig();
}

/**
* Resolve request host for proxy/NO_PROXY checks: config.host or default CMA from region.
* Use when the caller may omit host so NO_PROXY still applies (e.g. from region.cma).
* @param config - Object with optional host (e.g. API client config)
* @returns Host string (hostname or empty)
*/
export function resolveRequestHost(config: { host?: string }): string {
if (config.host) return config.host;
const cma = configStore.get('region')?.cma;
if (cma && typeof cma === 'string') {
if (cma.startsWith('http')) {
try {
const u = new URL(cma);
return u.hostname || cma;
} catch {
return cma;
}
}
return cma;
}
return '';
}

/**
* Temporarily clear proxy-related env vars so SDK/axios cannot use them.
* Call the returned function to restore. Use when creating a client for a host in NO_PROXY.
* @returns Restore function (call to put env back)
*/
export function clearProxyEnv(): () => void {
const saved: Record<string, string | undefined> = {};
const keys = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy'];
for (const k of keys) {
if (k in process.env) {
saved[k] = process.env[k];
delete process.env[k];
}
}
return () => {
for (const k of keys) {
if (saved[k] !== undefined) process.env[k] = saved[k];
}
};
}

/**
* Check if proxy is configured (from any source)
* @returns true if proxy is configured, false otherwise
Expand Down
8 changes: 4 additions & 4 deletions packages/contentstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@
"dependencies": {
"@contentstack/cli-audit": "~1.19.0-beta.0",
"@contentstack/cli-cm-export": "~1.24.0-beta.0",
"@contentstack/cli-cm-import": "~1.32.0-beta.1",
"@contentstack/cli-cm-import": "~1.32.0-beta.0",
"@contentstack/cli-auth": "~1.8.0-beta.0",
"@contentstack/cli-cm-bootstrap": "~1.19.0-beta.1",
"@contentstack/cli-cm-bootstrap": "~1.19.0-beta.0",
"@contentstack/cli-cm-branches": "~1.7.0-beta.0",
"@contentstack/cli-cm-bulk-publish": "~1.11.0-beta.0",
"@contentstack/cli-cm-clone": "~1.21.0-beta.1",
"@contentstack/cli-cm-clone": "~1.21.0-beta.0",
"@contentstack/cli-cm-export-to-csv": "~1.12.0-beta.0",
"@contentstack/cli-cm-import-setup": "~1.8.0-beta.0",
"@contentstack/cli-cm-migrate-rte": "~1.6.4",
"@contentstack/cli-cm-seed": "~1.15.0-beta.2",
"@contentstack/cli-cm-seed": "~1.15.0-beta.0",
"@contentstack/cli-command": "~1.8.0-beta.0",
"@contentstack/cli-config": "~1.20.0-beta.0",
"@contentstack/cli-launch": "^1.9.6",
Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading