Skip to content

DX | 04-08-2025 | Release #410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
499c7ba
feat: add robust error handling for transient network failures
nadeem-cs Jul 23, 2025
fd1a567
fix: existing test cases for concurrency
nadeem-cs Jul 24, 2025
9b4bce0
chore: Validate and sanitize URL before making request for SSRF preve…
nadeem-cs Jul 24, 2025
dd4661e
feat: adds jitter to networkRetryDelay
nadeem-cs Jul 24, 2025
d468866
fix: retryNetworkError method to update the running queue using shifts
nadeem-cs Jul 24, 2025
dbb940b
chore: improve sanitization on concurreny queue logic
nadeem-cs Jul 25, 2025
fbdadfd
Merge pull request #402 from contentstack/master
harshithad0703 Jul 28, 2025
2a3fb9d
Merge pull request #403 from contentstack/staging
harshithad0703 Jul 28, 2025
d4a538c
Dependency Update and talisman rc
cs-raj Jul 29, 2025
0e0f9f0
Merge branch 'development' into fix/DX-2370
cs-raj Jul 29, 2025
ba63130
Lock file update
cs-raj Jul 29, 2025
83bfa98
Added api_version support in get job status method.
sunil-lakshman Jul 29, 2025
508ad2e
fix the secret issue
sunil-lakshman Jul 29, 2025
c0a9654
Fixed linting errors
sunil-lakshman Jul 29, 2025
a7092c6
Fixed sanity testcases and removed consolelogs
sunil-lakshman Jul 29, 2025
72c25a7
Merge pull request #405 from contentstack/enh/dx-2181
sunil-lakshman Jul 29, 2025
24bb43f
commenting these tests out as these are yet to be validated
harshithad0703 Jul 29, 2025
f2b7e80
Merge pull request #406 from contentstack/fix/sanity-test-job-status
AniketDev7 Jul 29, 2025
6daeb42
Merge branch 'development' into fix/DX-2370
cs-raj Jul 30, 2025
5ca58c1
Lock file update
cs-raj Jul 30, 2025
f43ae6a
Merge pull request #404 from contentstack/fix/DX-2370
cs-raj Jul 30, 2025
9b0208f
Merge branch 'development' into enhancement/DX-3178
nadeem-cs Jul 30, 2025
5e53ee7
Added sanity testcases.
sunil-lakshman Jul 30, 2025
4a446ae
Merge pull request #395 from contentstack/enhancement/DX-3178
nadeem-cs Jul 30, 2025
d42e2c7
Fixed lint errors
sunil-lakshman Jul 30, 2025
1f887e0
Merge pull request #407 from contentstack/enh/dx-2181-sanity
sunil-lakshman Jul 30, 2025
7078bde
Added delay in bulk publish test suites
sunil-lakshman Jul 30, 2025
14e7390
Fixed the lint errors
sunil-lakshman Jul 30, 2025
60d11dd
Merge pull request #408 from contentstack/fix/dx-2181-sanity-delay
sunil-lakshman Jul 30, 2025
97b0321
snyk issue fix
cs-raj Aug 1, 2025
4336914
Merge pull request #411 from contentstack/fix/dev
cs-raj Aug 1, 2025
bf2b162
Merge pull request #409 from contentstack/development
harshithad0703 Aug 1, 2025
17b5841
Update .talismanrc
harshithad0703 Aug 1, 2025
591d49c
Merge pull request #412 from contentstack/fix/talismanrc
harshithad0703 Aug 1, 2025
bae744b
Change revert
cs-raj Aug 1, 2025
1248ec8
Merge pull request #413 from contentstack/revert/change
harshithad0703 Aug 1, 2025
4e8c599
Merge pull request #414 from contentstack/development
cs-raj Aug 1, 2025
c5f226d
fixed ssrf
cs-raj Aug 1, 2025
b68dd3f
Merge pull request #415 from contentstack/fix/snyk
AniketDev7 Aug 1, 2025
40906b7
Merge pull request #416 from contentstack/development
harshithad0703 Aug 4, 2025
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
25 changes: 22 additions & 3 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
fileignoreconfig:
- filename: test/unit/globalField-test.js
checksum: 25185e3400a12e10a043dc47502d8f30b7e1c4f2b6b4d3b8b55cdc19850c48bf
- filename: lib/stack/index.js
checksum: 6aab5edf85efb17951418b4dc4402889cd24c8d786c671185074aeb4d50f0242
- filename: test/sanity-check/api/stack-test.js
checksum: 198d5cf7ead33b079249dc3ecdee61a9c57453e93f1073ed0341400983e5aa53
- filename: .github/workflows/secrets-scan.yml
ignore_detectors:
- filecontent
- filename: package-lock.json
checksum: b043facad4b4aca7a013730746bdb9cb9e9dfca1e5d6faf11c068fc2525569c0
- filename: .husky/pre-commit
checksum: 52a664f536cf5d1be0bea19cb6031ca6e8107b45b6314fe7d47b7fad7d800632
- filename: test/sanity-check/api/user-test.js
checksum: 6bb8251aad584e09f4d963a913bd0007e5f6e089357a44c3fb1529e3fda5509d
- filename: package-lock.json
checksum: b9068b76378f5cedcae28adfff14b961289b3a0ddcd026fe3d026cfd877178a4
- filename: lib/stack/asset/index.js
checksum: b3358310e9cb2fb493d70890b7219db71e2202360be764465d505ef71907eefe
version: ""
- filename: test/sanity-check/api/previewToken-test.js
checksum: 9a42e079b7c71f76932896a0d2390d86ac626678ab20d36821dcf962820a886c
- filename: lib/stack/deliveryToken/index.js
checksum: 51ae00f07f4cc75c1cd832b311c2e2482f04a8467a0139da6013ceb88fbdda2f
- filename: lib/stack/deliveryToken/previewToken/index.js
checksum: b506f33bffdd20dfc701f964370707f5d7b28a2c05c70665f0edb7b3c53c165b
- filename: examples/robust-error-handling.js
checksum: e8a32ffbbbdba2a15f3d327273f0a5b4eb33cf84cd346562596ab697125bbbc6
- filename: test/sanity-check/api/bulkOperation-test.js
checksum: f40a14c84ab9a194aaf830ca68e14afde2ef83496a07d4a6393d7e0bed15fb0e
version: "1.0"
87 changes: 87 additions & 0 deletions examples/robust-error-handling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Example: Configuring Robust Error Handling for Transient Network Failures
// This example shows how to use the enhanced retry mechanisms in the Contentstack Management SDK

const contentstack = require('../lib/contentstack')

// Example 1: Basic configuration with enhanced network retry
const clientWithBasicRetry = contentstack.client({
api_key: 'your_api_key',
management_token: 'your_management_token',
// Enhanced network retry configuration
retryOnNetworkFailure: true, // Enable network failure retries
maxNetworkRetries: 3, // Max 3 attempts for network failures
networkRetryDelay: 100, // Start with 100ms delay
networkBackoffStrategy: 'exponential' // Use exponential backoff (100ms, 200ms, 400ms)
})

// Example 2: Advanced configuration with fine-grained control
const clientWithAdvancedRetry = contentstack.client({
api_key: 'your_api_key',
management_token: 'your_management_token',
// Network failure retry settings
retryOnNetworkFailure: true,
retryOnDnsFailure: true, // Retry on DNS resolution failures (EAI_AGAIN)
retryOnSocketFailure: true, // Retry on socket errors (ECONNRESET, ETIMEDOUT, etc.)
retryOnHttpServerError: true, // Retry on HTTP 5xx errors
maxNetworkRetries: 5, // Allow up to 5 network retries
networkRetryDelay: 200, // Start with 200ms delay
networkBackoffStrategy: 'exponential',

// Original retry settings (for non-network errors)
retryOnError: true,
retryLimit: 3,
retryDelay: 500,

// Custom logging
logHandler: (level, message) => {
console.log(`[${level.toUpperCase()}] ${new Date().toISOString()}: ${message}`)
}
})

// Example 3: Conservative configuration for production
const clientForProduction = contentstack.client({
api_key: 'your_api_key',
management_token: 'your_management_token',
// Conservative retry settings for production
retryOnNetworkFailure: true,
maxNetworkRetries: 2, // Only 2 retries to avoid long delays
networkRetryDelay: 300, // Longer initial delay
networkBackoffStrategy: 'fixed', // Fixed delay instead of exponential

// Custom retry condition for additional control
retryCondition: (error) => {
// Custom logic: only retry on specific conditions
return error.response && error.response.status >= 500
}
})

// Example usage with error handling
async function demonstrateRobustErrorHandling () {
try {
const stack = clientWithAdvancedRetry.stack('your_stack_api_key')
const contentTypes = await stack.contentType().query().find()
console.log('Content types retrieved successfully:', contentTypes.items.length)
} catch (error) {
if (error.retryAttempts) {
console.error(`Request failed after ${error.retryAttempts} retry attempts:`, error.message)
console.error('Original error:', error.originalError?.code)
} else {
console.error('Request failed:', error.message)
}
}
}

// The SDK will now automatically handle:
// ✅ DNS resolution failures (EAI_AGAIN)
// ✅ Socket errors (ECONNRESET, ETIMEDOUT, ECONNREFUSED)
// ✅ HTTP timeouts (ECONNABORTED)
// ✅ HTTP 5xx server errors (500-599)
// ✅ Exponential backoff with configurable delays
// ✅ Clear logging and user-friendly error messages

module.exports = {
clientWithBasicRetry,
clientWithAdvancedRetry,
clientForProduction,
demonstrateRobustErrorHandling
}
137 changes: 136 additions & 1 deletion lib/core/Util.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { platform, release } from 'os'
const HOST_REGEX = /^(?!\w+:\/\/)([\w-:]+\.)+([\w-:]+)(?::(\d+))?(?!:)$/
const HOST_REGEX = /^(?!(?:(?:https?|ftp):\/\/|internal|localhost|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)))(?:[\w-]+\.contentstack\.(?:io|com)(?::[^\/\s:]+)?|[\w-]+(?:\.[\w-]+)*(?::[^\/\s:]+)?)(?![\/?#])$/ // eslint-disable-line

export function isHost (host) {
if (!host) return false
return HOST_REGEX.test(host)
}

Expand Down Expand Up @@ -100,3 +101,137 @@ export default function getUserAgent (sdk, application, integration, feature) {

return `${headerParts.filter((item) => item !== '').join('; ')};`
}

// URL validation functions to prevent SSRF attacks
const isValidURL = (url) => {
try {
// Reject obviously malicious patterns early
if (url.includes('@') || url.includes('file://') || url.includes('ftp://')) {
return false
}

// Allow relative URLs (they are safe as they use the same origin)
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
return true
}

// Only validate absolute URLs for SSRF protection
const parsedURL = new URL(url)

// Reject non-HTTP(S) protocols
if (!['http:', 'https:'].includes(parsedURL.protocol)) {
return false
}

const officialDomains = [
'api.contentstack.io',
'eu-api.contentstack.com',
'azure-na-api.contentstack.com',
'azure-eu-api.contentstack.com',
'gcp-na-api.contentstack.com',
'gcp-eu-api.contentstack.com'
]
const isContentstackDomain = officialDomains.some(domain =>
parsedURL.hostname === domain || parsedURL.hostname.endsWith('.' + domain)
)
if (isContentstackDomain && parsedURL.protocol !== 'https:') {
return false
}

// Prevent IP addresses in URLs to avoid internal network access
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/
const ipv6Regex = /^\[?([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\]?$/
if (ipv4Regex.test(parsedURL.hostname) || ipv6Regex.test(parsedURL.hostname)) {
// Only allow localhost IPs in development
const isDevelopment = process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'test' ||
!process.env.NODE_ENV
const localhostIPs = ['127.0.0.1', '0.0.0.0', '::1', 'localhost']
if (!isDevelopment || !localhostIPs.includes(parsedURL.hostname)) {
return false
}
}

return isAllowedHost(parsedURL.hostname)
} catch {
// If URL parsing fails, it might be a relative URL without protocol
// Allow it if it doesn't contain protocol indicators or suspicious patterns
return !url?.includes('://') && !url?.includes('\\') && !url?.includes('@')
}
}

const isAllowedHost = (hostname) => {
// Define allowed domains for Contentstack API
// Official Contentstack domains
const allowedDomains = [
'api.contentstack.io',
'eu-api.contentstack.com',
'azure-na-api.contentstack.com',
'azure-eu-api.contentstack.com',
'gcp-na-api.contentstack.com',
'gcp-eu-api.contentstack.com'
]

// Check for localhost/development environments
const localhostPatterns = [
'localhost',
'127.0.0.1',
'0.0.0.0'
]

// Only allow localhost in development environments to prevent SSRF in production
const isDevelopment = process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'test' ||
!process.env.NODE_ENV // Default to allowing in non-production if NODE_ENV is not set

if (isDevelopment && localhostPatterns.includes(hostname)) {
return true
}

// Check if hostname is in allowed domains or is a subdomain of allowed domains
const isContentstackDomain = allowedDomains.some(domain => {
return hostname === domain || hostname.endsWith('.' + domain)
})

// If it's not a Contentstack domain, validate custom hostname
if (!isContentstackDomain) {
// Prevent internal/reserved IP ranges and localhost variants
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/
if (hostname?.match(ipv4Regex)) {
const parts = hostname.split('.')
const firstOctet = parseInt(parts[0])
// Only block private IP ranges
if (firstOctet === 10 || firstOctet === 192 || firstOctet === 127) {
return false
}
}
// Allow custom domains that don't match dangerous patterns
return !hostname.includes('file://') &&
!hostname.includes('\\') &&
!hostname.includes('@') &&
hostname !== 'localhost'
}

return isContentstackDomain
}

export const validateAndSanitizeConfig = (config) => {
if (!config?.url || typeof config?.url !== 'string') {
throw new Error('Invalid request configuration: missing or invalid URL')
}

// Validate the URL to prevent SSRF attacks
if (!isValidURL(config.url)) {
throw new Error(`SSRF Prevention: URL "${config.url}" is not allowed`)
}

// Additional validation for baseURL if present
if (config.baseURL && typeof config.baseURL === 'string' && !isValidURL(config.baseURL)) {
throw new Error(`SSRF Prevention: Base URL "${config.baseURL}" is not allowed`)
}

return {
...config,
url: config.url.trim() // Sanitize URL by removing whitespace
}
}
Loading
Loading