Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ea6bd95
Added testcases
sunil-lakshman Jan 23, 2026
3e29eb2
fixed workflows and updated lodash version
sunil-lakshman Jan 23, 2026
2790ca6
Added region and host config details in readme file
sunil-lakshman Jan 23, 2026
36ce251
feat: implement PKCE code_verifier storage in sessionStorage for brow…
nadeem-cs Feb 20, 2026
661e3e3
Merge pull request #509 from contentstack/enhancement/DX-4341
nadeem-cs Feb 23, 2026
59f498b
Merge pull request #512 from contentstack/master
naman-contentstack Feb 23, 2026
c9bfea4
enh: added support for get job item status
sunil-lakshman Feb 25, 2026
811e9a2
Merge pull request #514 from contentstack/enh/dx-3320
sunil-lakshman Feb 25, 2026
c2d4562
Merge pull request #491 from contentstack/enh/dx-3904
harshithad0703 Feb 27, 2026
4e292d5
Merge branch 'master' into fix/branch-strategy-phase1
harshithad0703 Feb 27, 2026
f50a2d5
fix: Update branch restrictions in PR comments and add version bump c…
harshithad0703 Feb 27, 2026
9cac6cd
Merge branch 'staging' into fix/branch-strategy-phase1
harshithad0703 Feb 27, 2026
9c2a813
changelog and version bump
harshithad0703 Feb 27, 2026
8966b14
feat: add post-checkout hook to merge latest main/master into new loc…
harshithad0703 Feb 27, 2026
3085d13
fix: add log message to post-checkout hook for existing branches on o…
harshithad0703 Feb 27, 2026
8a153cf
Merge pull request #515 from contentstack/fix/branch-strategy-phase1
harshithad0703 Feb 27, 2026
80ce042
remove setting.json
harshithad0703 Feb 27, 2026
445e313
Merge pull request #517 from contentstack/fix/remove-setting.json
harshithad0703 Feb 27, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ jsdocs
.early.coverage
# Snyk Security Extension - AI Rules (auto-generated)
.cursor/rules/snyk_rules.mdc
.vscode/settings.json
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."
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [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.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
Expand Down
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,39 @@ contentstackClient.stack({ api_key: 'API_KEY', management_token: 'MANAGEMENT_TOK
console.log(contenttype)
})
```

### Host and Region Configuration
You can configure the SDK to use a specific region or custom host for API requests.

#### Region
The SDK supports multiple regions. Valid region values are: `NA`, `EU`, `AU`, `AZURE_NA`, `AZURE_EU`, `GCP_NA`, `GCP_EU`. The default region is `NA`.

```javascript
// Use EU region
contentstackClient = contentstack.client({
authtoken: 'AUTHTOKEN',
region: 'EU'
})
```

#### Custom Host
You can specify a custom host for API requests. If both `host` and `region` are provided, the `host` parameter takes priority.

```javascript
// Use custom host
contentstackClient = contentstack.client({
authtoken: 'AUTHTOKEN',
host: 'api.contentstack.io'
})

// Custom host takes priority over region
contentstackClient = contentstack.client({
authtoken: 'AUTHTOKEN',
region: 'EU',
host: 'custom-api.example.com'
})
```

### Contentstack Management JavaScript SDK: 5-minute Quickstart
#### Initializing Your SDK:
To use the JavaScript CMA SDK, you need to first initialize it. To do this, use the following code:
Expand Down Expand Up @@ -124,7 +157,7 @@ contentstackClient.stack({ api_key: 'API_KEY' }).asset().create({ asset })
- [Content Management API Docs](https://www.contentstack.com/docs/developers/apis/content-management-api)

### The MIT License (MIT)
Copyright © 2012-2025 [Contentstack](https://www.contentstack.com/). All Rights Reserved
Copyright © 2012-2026 [Contentstack](https://www.contentstack.com/). All Rights Reserved

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
11 changes: 10 additions & 1 deletion lib/core/oauthHandler.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import errorFormatter from './contentstackError'
import { ERROR_MESSAGES } from './errorMessages'
import { getStoredCodeVerifier, storeCodeVerifier, clearStoredCodeVerifier } from './pkceStorage'

/**
* @description OAuthHandler class to handle OAuth authorization and token management
Expand Down Expand Up @@ -35,7 +36,13 @@ export default class OAuthHandler {

// Only generate PKCE codeVerifier and codeChallenge if clientSecret is not provided
if (!this.clientSecret) {
this.codeVerifier = this.generateCodeVerifier()
const stored = getStoredCodeVerifier(this.appId, this.clientId, this.redirectUri)
if (stored) {
this.codeVerifier = stored
} else {
this.codeVerifier = this.generateCodeVerifier()
storeCodeVerifier(this.appId, this.clientId, this.redirectUri, this.codeVerifier)
}
this.codeChallenge = null
}
}
Expand Down Expand Up @@ -139,8 +146,10 @@ export default class OAuthHandler {
const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/token`, body)

this._saveTokens(response.data)
clearStoredCodeVerifier(this.appId, this.clientId, this.redirectUri) // Clear immediately after successful exchange to prevent replay
return response.data
} catch (error) {
clearStoredCodeVerifier(this.appId, this.clientId, this.redirectUri) // Clear on error to prevent replay attacks
errorFormatter(error)
}
}
Expand Down
68 changes: 68 additions & 0 deletions lib/core/pkceStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* PKCE code_verifier persistence in sessionStorage for browser SPAs.
* Survives OAuth redirects; not used in Node. RFC 7636 / OAuth 2.0 for Browser-Based Apps.
*/

const PKCE_STORAGE_KEY_PREFIX = 'contentstack_oauth_pkce'
const PKCE_STORAGE_EXPIRY_MS = 10 * 60 * 1000 // 10 minutes

function isBrowser () {
return typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined'
}

function getStorageKey (appId, clientId, redirectUri) {
return `${PKCE_STORAGE_KEY_PREFIX}_${appId}_${clientId}_${redirectUri}`
}

/**
* @param {string} appId
* @param {string} clientId
* @param {string} redirectUri
* @returns {string|null} code_verifier if valid and not expired, otherwise null
*/
export function getStoredCodeVerifier (appId, clientId, redirectUri) {
if (!isBrowser()) return null
try {
const raw = window.sessionStorage.getItem(getStorageKey(appId, clientId, redirectUri))
if (!raw) return null
const { codeVerifier, expiresAt } = JSON.parse(raw)
if (!codeVerifier || !expiresAt || Date.now() > expiresAt) return null
return codeVerifier
} catch {
return null
}
}

/**
* @param {string} appId
* @param {string} clientId
* @param {string} redirectUri
* @param {string} codeVerifier
*/
export function storeCodeVerifier (appId, clientId, redirectUri, codeVerifier) {
if (!isBrowser()) return
try {
const key = getStorageKey(appId, clientId, redirectUri)
const value = JSON.stringify({
codeVerifier,
expiresAt: Date.now() + PKCE_STORAGE_EXPIRY_MS
})
window.sessionStorage.setItem(key, value)
} catch {
// Ignore storage errors (e.g. private mode); fall back to memory-only
}
}

/**
* @param {string} appId
* @param {string} clientId
* @param {string} redirectUri
*/
export function clearStoredCodeVerifier (appId, clientId, redirectUri) {
if (!isBrowser()) return
try {
window.sessionStorage.removeItem(getStorageKey(appId, clientId, redirectUri))
} catch {
// Ignore
}
}
43 changes: 43 additions & 0 deletions lib/stack/bulkOperation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,49 @@ export function BulkOperation (http, data = {}) {
}
}

/**
* The getJobItems request allows you to get the items of a bulk job.
* Response structure varies based on query params: items (always), skip/limit/total_count (when include_count=true), and other fields per params.
* @memberof BulkOperation
* @func getJobItems
* @returns {Promise<Object>} Response Object. Structure varies with params - always includes items array; may include skip, limit, total_count when include_count=true.
* @param {String} job_id - The ID of the job.
* @param {Object} [params={}] - Query parameters. Supports: include_count, skip, limit, include_reference, status, type, ct (content type UID or array), api_version, and any other dynamic query params.
* @example
* client.stack({ api_key: 'api_key'}).bulkOperation().getJobItems('job_id')
* .then((response) => { console.log(response) })
* @example
* client.stack({ api_key: 'api_key'}).bulkOperation().getJobItems('job_id', { skip: 0, limit: 50, include_count: true })
* .then((response) => { console.log(response) })
*/
// eslint-disable-next-line camelcase
this.getJobItems = async (job_id, params = {}) => {
// eslint-disable-next-line camelcase
const { api_version = '3.2', ...queryParams } = cloneDeep(params)
// eslint-disable-next-line camelcase
this.urlPath = `/bulk/jobs/${job_id}/items`
const headers = {
headers: {
...cloneDeep(this.stackHeaders)
}
}
// eslint-disable-next-line camelcase
if (api_version) headers.headers.api_version = api_version
if (Object.keys(queryParams).length > 0) headers.params = queryParams
try {
const response = await http.get(this.urlPath, headers)
if (response.data) {
// eslint-disable-next-line camelcase
if (api_version) delete headers.headers.api_version
return response.data
}
} catch (error) {
// eslint-disable-next-line camelcase
if (api_version) delete headers.headers.api_version
console.error(error)
}
}

/**
* The Publish entries and assets in bulk request allows you to publish multiple entries and assets at the same time.
* @memberof BulkOperation
Expand Down
Loading
Loading