From d0338ec88a20c561202d09e0c56a1baa380b941f Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:33:10 +0530 Subject: [PATCH 1/2] Added NO_PROXY env support --- .../src/contentstack-management-sdk.ts | 16 ++- .../src/http-client/client.ts | 22 ++-- .../src/proxy-helper.ts | 116 ++++++++++-------- 3 files changed, 91 insertions(+), 63 deletions(-) diff --git a/packages/contentstack-utilities/src/contentstack-management-sdk.ts b/packages/contentstack-utilities/src/contentstack-management-sdk.ts index 77600cc31f..468996114c 100644 --- a/packages/contentstack-utilities/src/contentstack-management-sdk.ts +++ b/packages/contentstack-utilities/src/contentstack-management-sdk.ts @@ -2,7 +2,12 @@ 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 { getProxyConfigForHost, resolveRequestHost, clearProxyEnv } from './proxy-helper'; +import { + getProxyConfigForHost, + resolveRequestHost, + clearProxyEnv, + shouldBypassProxy, +} from './proxy-helper'; import dotenv from 'dotenv'; dotenv.config(); @@ -22,13 +27,13 @@ class ManagementSDKInitiator { // 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) { + // When NO_PROXY matches, strip proxy env so SDK/axios cannot pick up HTTP_PROXY for this process. + if (host && shouldBypassProxy(host)) { clearProxyEnv(); } const option: ContentstackConfig = { - host: config.host, + host: config.host || host || undefined, maxContentLength: config.maxContentLength || 100000000, maxBodyLength: config.maxBodyLength || 1000000000, maxRequests: 10, @@ -118,7 +123,10 @@ class ManagementSDKInitiator { if (proxyConfig) { option.proxy = proxyConfig; + } else if (host && shouldBypassProxy(host)) { + option.proxy = false; } + // When host is in NO_PROXY, do not add proxy to option at all if (config.endpoint) { option.endpoint = config.endpoint; } diff --git a/packages/contentstack-utilities/src/http-client/client.ts b/packages/contentstack-utilities/src/http-client/client.ts index 2fd213d2ed..c96a890152 100644 --- a/packages/contentstack-utilities/src/http-client/client.ts +++ b/packages/contentstack-utilities/src/http-client/client.ts @@ -3,19 +3,25 @@ import { IHttpClient } from './client-interface'; import { HttpResponse } from './http-response'; import configStore from '../config-handler'; import authHandler from '../auth-handler'; -import { hasProxy, getProxyUrl, getProxyConfig, getProxyConfigForHost } from '../proxy-helper'; +import { + hasProxy, + getProxyUrl, + getProxyConfigForHost, + resolveRequestHost, + shouldBypassProxy, +} 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) { + for (const u of toTry) { try { - const parsed = new URL(candidateUrl.startsWith('http') ? candidateUrl : `https://${candidateUrl}`); + const parsed = new URL(u.startsWith('http') ? u : `https://${u}`); return parsed.hostname || undefined; } catch { - // Invalid URL; try next candidate (baseURL or url) + // ignore } } return undefined; @@ -427,12 +433,14 @@ export class HttpClient implements IHttpClient { } } - // Configure proxy if available. NO_PROXY has priority: hosts in NO_PROXY never use proxy. + // Configure proxy if available. NO_PROXY has priority; fall back to region CMA for host resolution. if (!this.request.proxy) { - const host = getRequestHost(this.request.baseURL, url); - const proxyConfig = host ? getProxyConfigForHost(host) : getProxyConfig(); + const host = getRequestHost(this.request.baseURL, url) || resolveRequestHost({}); + const proxyConfig = getProxyConfigForHost(host); if (proxyConfig) { this.request.proxy = proxyConfig; + } else if (host && shouldBypassProxy(host)) { + this.request.proxy = false; } } diff --git a/packages/contentstack-utilities/src/proxy-helper.ts b/packages/contentstack-utilities/src/proxy-helper.ts index 68de79fc8d..7abd8eda74 100644 --- a/packages/contentstack-utilities/src/proxy-helper.ts +++ b/packages/contentstack-utilities/src/proxy-helper.ts @@ -82,44 +82,14 @@ export function shouldBypassProxy(host: string): boolean { } /** - * Get proxy configuration. Sources (in order): env (HTTP_PROXY/HTTPS_PROXY), then global config - * from `csdx config:set:proxy --host --port --protocol `. + * Get proxy configuration. Priority order (per spec): + * 1. Global CLI config from `csdx config:set:proxy --host --port --protocol ` + * 2. Environment variables (HTTPS_PROXY or HTTP_PROXY) * 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: Environment variables (HTTPS_PROXY or HTTP_PROXY) - const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; - - if (proxyUrl) { - try { - const url = new URL(proxyUrl); - const defaultPort = url.protocol === 'https:' ? 443 : 80; - const port = url.port ? Number.parseInt(url.port, 10) : defaultPort; - - if (!Number.isNaN(port) && port >= 1 && port <= 65535) { - const protocol = url.protocol.replace(':', '') as 'http' | 'https'; - const proxyConfig: ProxyConfig = { - protocol: protocol, - host: url.hostname, - port: port, - }; - - if (url.username || url.password) { - proxyConfig.auth = { - username: url.username, - password: url.password, - }; - } - - return proxyConfig; - } - } catch { - // Invalid URL, continue to check global config - } - } - - // Priority 2: Global config (csdx config:set:proxy) + // Priority 1: Global config (csdx config:set:proxy) const globalProxyConfig = configStore.get('proxy'); if (globalProxyConfig) { if (typeof globalProxyConfig === 'object') { @@ -151,11 +121,42 @@ export function getProxyConfig(): ProxyConfig | undefined { return proxyConfig; } } catch { - // Invalid URL, return undefined + // Invalid URL, continue to check environment } } } + // Priority 2: Environment variables (HTTPS_PROXY or HTTP_PROXY) + const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; + + if (proxyUrl) { + try { + const url = new URL(proxyUrl); + const defaultPort = url.protocol === 'https:' ? 443 : 80; + const port = url.port ? Number.parseInt(url.port, 10) : defaultPort; + + if (!Number.isNaN(port) && port >= 1 && port <= 65535) { + const protocol = url.protocol.replace(':', '') as 'http' | 'https'; + const proxyConfig: ProxyConfig = { + protocol: protocol, + host: url.hostname, + port: port, + }; + + if (url.username || url.password) { + proxyConfig.auth = { + username: url.username, + password: url.password, + }; + } + + return proxyConfig; + } + } catch { + // Invalid URL, return undefined + } + } + return undefined; } @@ -172,27 +173,38 @@ export function getProxyConfigForHost(host: string): ProxyConfig | undefined { return getProxyConfig(); } +function regionCmaHostname(): string { + const cma = configStore.get('region')?.cma; + if (!cma || typeof cma !== 'string') { + return ''; + } + if (cma.startsWith('http')) { + try { + const u = new URL(cma); + return u.hostname || cma; + } catch { + return cma; + } + } + return cma; +} + /** - * 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) + * Hostname for NO_PROXY / proxy. Prefer `region.cma` when set so callers that pass a + * default SDK host (e.g. bulk-entries -> api.contentstack.io) still match rules like + * `.csnonprod.com` against the real API host (e.g. dev11-api.csnonprod.com). */ 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; + const fromRegion = regionCmaHostname(); + if (fromRegion) { + return normalizeHost(fromRegion) || fromRegion; + } + + const raw = config.host?.trim() || ''; + if (!raw) { + return ''; } - return ''; + return normalizeHost(raw) || raw; } /** From cad030f6b369f016109f24d7fe2d8e231b6bc5e1 Mon Sep 17 00:00:00 2001 From: sunil-lakshman <104969541+sunil-lakshman@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:37:16 +0530 Subject: [PATCH 2/2] fix: updated brace-expansion version bump --- .talismanrc | 2 +- pnpm-lock.yaml | 42 +++++++++--------------------------------- pnpm-workspace.yaml | 1 + 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/.talismanrc b/.talismanrc index 74748021e5..ca301d9f24 100644 --- a/.talismanrc +++ b/.talismanrc @@ -2,5 +2,5 @@ fileignoreconfig: - filename: .github/workflows/release-production-pipeline.yml checksum: 48264fdeb61cbbed348c7271aae5e155651f490aca063dbb1d54f2c15a154090 - filename: pnpm-lock.yaml - checksum: 86aa5ecf1bce4a09ae5ee3027c28664b749bd95eaabffc51449ab4bfd99bfe69 + checksum: 3143f78aa6c83f0d41ec24a2bb7f25dce0ae25d969ee9fb6bf0787a85773e60a version: '1.0' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ad89037cf..7b1d2ad56a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: picomatch: 4.0.4 + brace-expansion: 5.0.5 importers: @@ -2299,9 +2300,6 @@ packages: axios@1.13.6: resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -2337,14 +2335,8 @@ packages: bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -2587,9 +2579,6 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concat-stream@2.0.0: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} @@ -8458,8 +8447,6 @@ snapshots: transitivePeerDependencies: - debug - balanced-match@1.0.2: {} - balanced-match@4.0.4: {} base64-js@1.5.1: {} @@ -8506,16 +8493,7 @@ snapshots: bowser@2.14.1: {} - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - brace-expansion@5.0.4: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -8790,8 +8768,6 @@ snapshots: commondir@1.0.1: {} - concat-map@0.0.1: {} - concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 @@ -10748,23 +10724,23 @@ snapshots: minimatch@10.2.4: dependencies: - brace-expansion: 5.0.4 + brace-expansion: 5.0.5 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 5.0.5 minimatch@5.1.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.5 minimatch@9.0.3: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.5 minimatch@9.0.9: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.5 minimist@1.2.8: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4ac3fe84fa..721daf7701 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - 'packages/*' overrides: picomatch: 4.0.4 + brace-expansion: 5.0.5