Skip to content
Merged
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: 4 additions & 4 deletions .github/workflows/check-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Comment PR
if: github.base_ref == 'master' && github.head_ref != 'staging'
if: github.base_ref == 'master' && github.head_ref != 'developement'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the next branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch.
We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the development branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch.
- name: Check branch
if: github.base_ref == 'master' && github.head_ref != 'staging'
if: github.base_ref == 'master' && github.head_ref != 'development'
run: |
echo "ERROR: We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the next branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch."
echo "ERROR: We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the development branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch."
exit 1
58 changes: 58 additions & 0 deletions .github/workflows/check-version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Ensures package.json and CHANGELOG.md are bumped compared to the latest tag when relevant files change.
name: Check Version Bump

on:
pull_request:
paths:
- 'package.json'
- 'CHANGELOG.md'

jobs:
version-bump:
name: Version & Changelog bump
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22.x'

- name: Check version bump
run: |
set -e
PKG_VERSION=$(node -p "require('./package.json').version.replace(/^v/, '')")
if [ -z "$PKG_VERSION" ]; then
echo "::error::Could not read version from package.json"
exit 1
fi
git fetch --tags --force 2>/dev/null || true
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true)
if [ -z "$LATEST_TAG" ]; then
echo "No existing tags found. Skipping version-bump check (first release)."
exit 0
fi
LATEST_VERSION="${LATEST_TAG#v}"
LATEST_VERSION="${LATEST_VERSION%%-*}"
if [ "$(printf '%s\n' "$LATEST_VERSION" "$PKG_VERSION" | sort -V | tail -1)" != "$PKG_VERSION" ]; then
echo "::error::Version bump required: package.json version ($PKG_VERSION) is not greater than latest tag ($LATEST_TAG). Please bump the version in package.json."
exit 1
fi
if [ "$PKG_VERSION" = "$LATEST_VERSION" ]; then
echo "::error::Version bump required: package.json version ($PKG_VERSION) equals latest tag ($LATEST_TAG). Please bump the version in package.json."
exit 1
fi
CHANGELOG_VERSION=$(sed -nE 's/^## \[v?([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' CHANGELOG.md | head -1)
if [ -z "$CHANGELOG_VERSION" ]; then
echo "::error::Could not find a version entry in CHANGELOG.md (expected line like '## [v1.0.0](...)')."
exit 1
fi
if [ "$CHANGELOG_VERSION" != "$PKG_VERSION" ]; then
echo "::error::CHANGELOG version mismatch: CHANGELOG.md top version ($CHANGELOG_VERSION) does not match package.json version ($PKG_VERSION). Please add or update the CHANGELOG entry for $PKG_VERSION."
exit 1
fi
echo "Version bump check passed: package.json and CHANGELOG.md are at $PKG_VERSION (latest tag: $LATEST_TAG)."
1 change: 0 additions & 1 deletion .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
branches:
- master
- main
- staging
- development
jobs:
build-test:
Expand Down
40 changes: 40 additions & 0 deletions .husky/post-checkout
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env sh
# When switching to a branch that doesn't exist on remote (e.g. newly created),
# pull and merge origin/main or origin/master into current branch. Does not push.

# Only run on branch checkout (not file checkout)
if [ "$3" != "1" ]; then
exit 0
fi

# Skip if we don't have a remote
if ! git rev-parse --verify origin 2>/dev/null; then
exit 0
fi

CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)

# Skip main/master/development - no need to merge base into these
case "$CURRENT_BRANCH" in
main|master|development) exit 0 ;;
esac

# Only run when current branch does not exist on origin (treat as new local branch)
if git ls-remote --heads origin "$CURRENT_BRANCH" 2>/dev/null | grep -q .; then
echo "post-checkout: $CURRENT_BRANCH exists on origin, skipping merge."
exit 0
fi

# Prefer main, fallback to master
if git rev-parse --verify origin/main 2>/dev/null; then
BASE=origin/main
elif git rev-parse --verify origin/master 2>/dev/null; then
BASE=origin/master
else
exit 0
fi

echo "New branch detected: merging latest $BASE into $CURRENT_BRANCH (local only, not pushing)..."
git fetch origin
git merge "$BASE" --no-edit --no-ff
echo "Done. Merge is local only; push when ready."
16 changes: 5 additions & 11 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
fileignoreconfig:
- filename: lib/core/pkceStorage.js
checksum: e060690c5ed348a6914df7ecc36de5b6b45f9a7c3a9c164c88bd2c7fad2bea08
- filename: test/unit/oauthHandler-test.js
checksum: 95a968c0d72d5bbe9e1acb30ea17ab505938f6174e917d7a25dda8facfda5a49
- filename: test/unit/pkceStorage-test.js
checksum: 567f557d37e8119c22cd4c5c4014c16dd660c03be35f65e803fb340cfd4b2136
- filename: lib/contentstackClient.js
checksum: f564f6eee5c17dc73abdeab4be226a3b37942893e149d907d2a4ef415c485c5e
- filename: test/unit/globalField-test.js
checksum: 25185e3400a12e10a043dc47502d8f30b7e1c4f2b6b4d3b8b55cdc19850c48bf
- filename: lib/stack/index.js
Expand All @@ -15,7 +11,9 @@ fileignoreconfig:
ignore_detectors:
- filecontent
- filename: package-lock.json
checksum: 47d7cb6b4cd8701aa289aa2e3975162b1291d4c8a60725d21c103a1ba59df201
checksum: 4a58eb4ee1f54d68387bd005fb76e83a02461441c647d94017743d3442c0f476
- filename: test/unit/ContentstackClient-test.js
checksum: 5d8519b5b93c715e911a62b4033614cc4fb3596eabf31c7216ecb4cc08604a73
- filename: .husky/pre-commit
checksum: 52a664f536cf5d1be0bea19cb6031ca6e8107b45b6314fe7d47b7fad7d800632
- filename: test/sanity-check/api/user-test.js
Expand All @@ -32,10 +30,6 @@ fileignoreconfig:
checksum: e8a32ffbbbdba2a15f3d327273f0a5b4eb33cf84cd346562596ab697125bbbc6
- filename: test/sanity-check/api/bulkOperation-test.js
checksum: f40a14c84ab9a194aaf830ca68e14afde2ef83496a07d4a6393d7e0bed15fb0e
- filename: lib/contentstackClient.js
checksum: b76ca091caa3a1b2658cd422a2d8ef3ac9996aea0aff3f982d56bb309a3d9fde
- filename: test/unit/ContentstackClient-test.js
checksum: 974a4f335aef025b657d139bb290233a69bed1976b947c3c674e97baffe4ce2f
- filename: test/unit/ContentstackHTTPClient-test.js
checksum: 4043efd843e24da9afd0272c55ef4b0432e3374b2ca12b913f1a6654df3f62be
- filename: test/unit/contentstack-test.js
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"snyk.advanced.autoSelectOrganization": false
}
17 changes: 10 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# Changelog

## [v1.27.6](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.6) (2026-03-02)
- Enhancement
- Added support for get job item status.
## [v1.28.0](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-03-02)
- Enh
- Added DAM 2.0 query support

## [v1.27.5](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-02-16)
- Enhancement
- OAuth PKCE: store code_verifier in sessionStorage (browser only) so token exchange works after redirect in React and other SPAs; verifier is restored on callback, cleared after successful exchange or on error; 10-minute expiry; Node remains memory-only
- Extracted PKCE storage into `lib/core/pkceStorage.js` module
## [v1.27.6](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-02-23)
- Fix
- Skip token refresh on 401 when API returns error_code 161 (environment/permission) so the actual API error is returned instead of triggering refresh and a generic "Unable to refresh token" message
- When token refresh fails after a 401, return the original API error (error_message, error_code) instead of the generic "Unable to refresh token" message

## [v1.27.5](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.5) (2026-02-11)
- Fix
- Concurrency queue: when response errors have no `config` (e.g. after network retries exhaust in some environments, or when plugins return a new error object), the SDK now rejects with a catchable Error instead of throwing an unhandled TypeError and crashing the process
- Hardened `responseHandler` to safely handle errors without `config` (e.g. plugin-replaced errors) by guarding `config.onComplete` and still running queue `shift()` so rejections remain catchable
- Added optional chaining for `error.config` reads in the retry path and unit tests for missing-config scenarios

## [v1.27.4](https://github.com/contentstack/contentstack-management-javascript/tree/v1.27.4) (2026-02-02)
- Fix
- Removed content-type header from the release delete method
Expand Down
51 changes: 36 additions & 15 deletions lib/core/concurrency-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
logFinalFailure(errorInfo, this.config.maxNetworkRetries)
// Final error message
const finalError = new Error(`Network request failed after ${this.config.maxNetworkRetries} retries: ${errorInfo.reason}`)
finalError.code = error.code
finalError.code = error && error.code
finalError.originalError = error
finalError.retryAttempts = attempt - 1
return Promise.reject(finalError)
Expand All @@ -181,6 +181,16 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
const delay = calculateNetworkRetryDelay(attempt)
logRetryAttempt(errorInfo, attempt, delay)

// Guard: retry failures (e.g. from nested retries) may not have config in some
// environments. Reject with a catchable error instead of throwing TypeError.
if (!error || !error.config) {
const finalError = new Error(`Network request failed after retries: ${errorInfo.reason}`)
finalError.code = error && error.code
finalError.originalError = error
finalError.retryAttempts = attempt - 1
return Promise.reject(finalError)
}

// Initialize retry count if not present
if (!error.config.networkRetryCount) {
error.config.networkRetryCount = 0
Expand All @@ -200,9 +210,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
safeAxiosRequest(requestConfig)
.then((response) => {
// On successful retry, call the original onComplete to properly clean up
if (error.config.onComplete) {
error.config.onComplete()
}
error?.config?.onComplete?.()
shift() // Process next queued request
resolve(response)
})
Expand All @@ -214,17 +222,13 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
.then(resolve)
.catch((finalError) => {
// On final failure, clean up the running queue
if (error.config.onComplete) {
error.config.onComplete()
}
error?.config?.onComplete?.()
shift() // Process next queued request
reject(finalError)
})
} else {
// On non-retryable error, clean up the running queue
if (error.config.onComplete) {
error.config.onComplete()
}
error?.config?.onComplete?.()
shift() // Process next queued request
reject(retryError)
}
Expand Down Expand Up @@ -446,9 +450,12 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
}
})
}
// Response interceptor used for
// Response interceptor used for success and for error path (Promise.reject(responseHandler(err))).
// When used with an error, err may lack config (e.g. plugin returns new error). Guard so we don't throw.
const responseHandler = (response) => {
response.config.onComplete()
if (response?.config?.onComplete) {
response.config.onComplete()
}
shift()
return response
}
Expand Down Expand Up @@ -478,13 +485,27 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
}

const responseErrorHandler = error => {
let networkError = error.config.retryCount
// Guard: Axios errors normally have config; missing config can occur when a retry
// fails in certain environments or when non-Axios errors propagate (e.g. timeouts).
// Reject with a catchable error instead of throwing TypeError and crashing the process.
if (!error || !error.config) {
const fallbackError = new Error(
error && typeof error.message === 'string'
? error.message
: 'Network request failed: error object missing request config'
)
fallbackError.code = error?.code
fallbackError.originalError = error
return Promise.reject(runPluginOnResponseForError(fallbackError))
}

let networkError = error?.config?.retryCount ?? 0
let retryErrorType = null

// First, check for transient network errors
const networkErrorInfo = isTransientNetworkError(error)
if (networkErrorInfo && this.config.retryOnNetworkFailure) {
const networkRetryCount = error.config.networkRetryCount || 0
const networkRetryCount = error?.config?.networkRetryCount || 0
return retryNetworkError(error, networkErrorInfo, networkRetryCount + 1)
}

Expand All @@ -499,7 +520,7 @@ export function ConcurrencyQueue ({ axios, config, plugins = [] }) {
var response = error.response
if (!response) {
if (error.code === 'ECONNABORTED') {
const timeoutMs = error.config.timeout || this.config.timeout || 'unknown'
const timeoutMs = error?.config?.timeout || this.config.timeout || 'unknown'
error.response = {
...error.response,
status: 408,
Expand Down
Loading
Loading