Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0121245
Merge pull request #1064 from marklogic/release/4.1.0
rjrudin Feb 23, 2026
04ef078
Reapply "MLE-25586 - add transitive closure function to node client."
stevebio Feb 23, 2026
a5b62de
Reapply "MLE-26340 expose vec.precision and vec.trunc in Node client."
stevebio Feb 23, 2026
4e4afee
Reapply "MLE-23744 - implement optic fromDocs in node."
stevebio Feb 23, 2026
562abe4
MLE-12345
stevebio Feb 23, 2026
91bbfbc
MLE-27299 Upgrade dependencies to remove vulnerability warnings.
stevebio Feb 24, 2026
17182f1
MLE-27299 Bumping dependencies to fix npm audit
rjrudin Mar 2, 2026
00fc4eb
SECCMP-1797: Add top-level permissions to restrict default token
GAdityaVarma Apr 8, 2026
ce2461b
Merge pull request #1070 from marklogic/fix/SECCMP-1797-harden-permis…
rjrudin Apr 8, 2026
dfb1947
PDP-1182: Remove per-repo pr-workflow.yaml
SameeraPriyathamTadikonda Apr 9, 2026
69fb7af
Merge pull request #1071 from marklogic/PDP-1182-remove-pr-workflow-f…
GAdityaVarma Apr 9, 2026
76d5732
MLE-28498 11.3.5 Test Fixes for Security Update (#1074)
jonmille May 11, 2026
16e8f7d
MLE-27883 adapt cts.param in the Optic API for MLS 12.1 (#1075)
RitaChen609 May 13, 2026
362cb16
MLE-28335 added fragment option in fromSearch (#1077)
RitaChen609 May 18, 2026
be35792
MLE-28583 xdmp.uriContentType and xdmp.uriFormat test fix (#1079)
jonmille May 21, 2026
c31886b
MLE-29694 fixes for vulnerabilities and flaky tests (#1080)
RitaChen609 May 26, 2026
a715199
MLE-30684 Configure Harness Artifact Registry (#1085)
jonmille Jun 23, 2026
b01655b
MLE-30686 MLE-30964 form-data and markdown-it dependency bumps (#1086)
jonmille Jun 30, 2026
f8cec86
MLE-29889 add param binding support for CTS queries in Optic plans
RitaChen609 Jun 30, 2026
794c2c6
MLE-30154 Pin Mocha to 11.7.5 (#1088)
jonmille Jul 1, 2026
2564a6c
MLE-29889 update integration tests
RitaChen609 Jul 1, 2026
04cf8c8
MLE-29889 add a test for a common use case for fromSearchDocs
RitaChen609 Jul 1, 2026
0cbb6a1
MLE-29889 restrict the bypass to only types.CtsQuery
RitaChen609 Jul 1, 2026
b4885f4
MLE-29889 refract the plan parameter substistution routine
RitaChen609 Jul 1, 2026
48ba286
MLE-29889 rework substitutePlanParams with one replacements map
RitaChen609 Jul 1, 2026
5742d47
MLE-29889 add test cases for mixed plain-string and CtsQuery bindings
RitaChen609 Jul 1, 2026
e4bdaf3
MLE-29889 refine test cases for orQuery test
RitaChen609 Jul 2, 2026
974962e
Merge pull request #1090 from marklogic/MLE-29889-param-binding-accep…
rjdew-progress Jul 2, 2026
e384a5c
MLE-30268 crypto.pseudoRandomBytes -> crypto.randomBytes
Jul 6, 2026
8f9bc7c
MLE-30265 add warning for BASIC auth without SSL
Jul 7, 2026
a4735a0
MLE-30256 Encode accessTokenDuration Before URL Interpolation in getA…
jonmille Jul 7, 2026
9f045ae
MLE-30257 Replace encodeURI With encodeURIComponent (#1094)
jonmille Jul 8, 2026
a7a86f3
Merge pull request #1093 from marklogic/MLE-30265
rjdew-progress Jul 8, 2026
7367a5b
Merge pull request #1092 from marklogic/MLE-30268
rjdew-progress Jul 8, 2026
c3c6bc9
MLE-30967 Update CODEOWNERS
rjdew-progress Jul 13, 2026
218b470
Update .copyrightconfig
rjdew-progress Jul 13, 2026
dfc99ea
Merge pull request #1095 from marklogic/rjdew-progress-patch-1
rjdew-progress Jul 14, 2026
b19c451
MLE-31133 MLE-31135 Bump brace-expansion & fast-uri (#1098)
jonmille Jul 14, 2026
0fb1c9f
MLE-30263: Fix Uncaught Exception in getAccessToken Error Handler
ngodugu-marklogic Jul 14, 2026
401c402
Merge pull request #1096 from marklogic/MLE-30263
ngodugu-marklogic Jul 16, 2026
2f9d2d3
docs(security): Document MD5 usage in HTTP Digest Auth (RFC 2617)
ngodugu-marklogic Aug 13, 2026
b568a4c
fix(security): Complete MD5 containment strategy (Issue #1109)
ngodugu-marklogic Aug 13, 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
2 changes: 1 addition & 1 deletion .copyrightconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ startyear: 2015
# - Dotfiles already skipped automatically
# Enable by removing the leading '# ' from the next line and editing values.
# filesexcluded: third_party/*, docs/generated/*.md, assets/*.png, scripts/temp_*.py, vendor/lib.js
filesexcluded: .github/*, README.md, Jenkinsfile, test-app/*, *.md, docker-compose.yaml, test-complete-app-mlDeploy/*, *.json, *.sh
filesexcluded: .github/*, README.md, Jenkinsfile, test-app/*, *.md, docker-compose.yaml, test-complete-app-mlDeploy/*, *.json, *.sh, CODEOWNERS
23 changes: 0 additions & 23 deletions .github/workflows/pr-workflow.yaml

This file was deleted.

5 changes: 5 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
engine-strict=true
registry=https://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-marklogic-npm/npm/
@jsr:registry=https://pkg.harness.io/pkg/ct8onj8YTdaXtKaFsYCRLg/org-marklogic-npm/npm/
ignore-scripts=true
# cooldown in days (14 days)
min-release-age=14
3 changes: 1 addition & 2 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
# Each line is a file pattern followed by one or more owners.

# These owners will be the default owners for everything in the repo.
* @anu3990 @billfarber @rjrudin @stevebio

* @rjrudin @jonmille @ngodugu-marklogic @RitaChen609 @rjdew-progress
5 changes: 3 additions & 2 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def runAuditReport() {
cd node-client-api
npm ci
rm -rf $WORKSPACE/npm-audit-report.json || true
npm audit --audit-level=moderate --json > $WORKSPACE/npm-audit-report.json
npm audit --audit-level=moderate --json > $WORKSPACE/npm-audit-report.json || true
'''
}

Expand Down Expand Up @@ -125,7 +125,7 @@ pipeline {
agent none

triggers {
parameterizedCron(env.BRANCH_NAME == "develop" ? "00 02 * * * % regressions=true" : "")
parameterizedCron(env.BRANCH_NAME == "develop" ? "00 05 * * * % regressions=true" : "")
}

parameters {
Expand All @@ -151,6 +151,7 @@ pipeline {
stage('pull-request-tests') {
agent { label 'nodeclientpool' }
steps {
// npm audit is non-blocking; Harness Artifact Repository does not currently support it so failures are ignored.
runAuditReport()
runLint()
runTypeCheck()
Expand Down
19 changes: 18 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,24 @@ module.exports = [
"no-console": "off",

// Bracket notation preference
"dot-notation": "error"
"dot-notation": "error",

// Security: Prevent MD5 imports outside www-authenticate-patched/ (Issue #1109)
"no-restricted-modules": ["error", {
"paths": [{
"name": "./lib/www-authenticate-patched/md5",
"message": "⚠️ SECURITY: Do not import MD5 from www-authenticate-patched. This is ONLY for HTTP Digest Auth (RFC 2617). Use bcrypt/scrypt/Argon2id for passwords, SHA-256/SHA-3 for general hashing."
}, {
"name": "./lib/www-authenticate-patched/md5.js",
"message": "⚠️ SECURITY: Do not import MD5 from www-authenticate-patched. This is ONLY for HTTP Digest Auth (RFC 2617). Use bcrypt/scrypt/Argon2id for passwords, SHA-256/SHA-3 for general hashing."
}, {
"name": "lib/www-authenticate-patched/md5",
"message": "⚠️ SECURITY: Do not import MD5 from www-authenticate-patched. This is ONLY for HTTP Digest Auth (RFC 2617). Use bcrypt/scrypt/Argon2id for passwords, SHA-256/SHA-3 for general hashing."
}, {
"name": "lib/www-authenticate-patched/md5.js",
"message": "⚠️ SECURITY: Do not import MD5 from www-authenticate-patched. This is ONLY for HTTP Digest Auth (RFC 2617). Use bcrypt/scrypt/Argon2id for passwords, SHA-256/SHA-3 for general hashing."
}]
}]
}
},
{
Expand Down
12 changes: 11 additions & 1 deletion etc/test-setup-users.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
var valcheck = require('core-util-is');

Expand Down Expand Up @@ -68,6 +68,16 @@ function setupUsers(manager, done) {
'privilege-name': 'xdmp-get-session-field',
action: 'http://marklogic.com/xdmp/privileges/xdmp-get-session-field',
kind: 'execute'
},
{
'privilege-name': 'xdmp-lock-acquire',
action: 'http://marklogic.com/xdmp/privileges/xdmp-lock-acquire',
kind: 'execute'
},
{
'privilege-name': 'xdmp-lock-release',
action: 'http://marklogic.com/xdmp/privileges/xdmp-lock-release',
kind: 'execute'
}
]
}
Expand Down
88 changes: 53 additions & 35 deletions lib/extlibs.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
'use strict';
const requester = require('./requester.js');
Expand Down Expand Up @@ -33,6 +33,49 @@ function emptyOutputTransform(/*headers, data*/) {
};
}

/**
* Safely encodes a caller-supplied extension library path for use in a REST
* request URL. Each path segment is encoded individually with
* encodeURIComponent(), preventing path-separator injection (CWE-22) and
* query-parameter injection (CWE-20). Any leading /v1/ext/, /ext/, or bare /
* prefix is stripped before splitting so that all three accepted input forms
* (bare name, /path/name, /ext/path/name) produce the same canonical output.
*
* @param {string} rawPath - caller-supplied path or directory
* @param {boolean} trailingSlash - when true, appends a trailing '/' (used by list())
* @returns {string} the encoded path beginning with /v1/ext/
* @throws {Error} if any segment equals '..' or '.'
* @ignore
*/
function encodeExtPath(rawPath, trailingSlash) {
if (typeof rawPath !== 'string' && !(rawPath instanceof String)) {
throw new Error('extension library path must be a string');
}

// Require a leading '/' before stripping any prefix so that undocumented
// bare forms like 'ext/module.xqy' are treated as literal path segments
// rather than silently having their 'ext/' prefix removed.
const stripped = rawPath
.replace(/^\/(?:(?:v1\/)?ext\/)?/, '')
.replace(/\/$/, '');

const segments = stripped.length === 0 ? [] : stripped.split('/');

for (const seg of segments) {
if (seg === '..' || seg === '.') {
throw new Error(
'extension library path must not contain relative path components (".", ".."): ' + rawPath
);
}
}

// Build the result using a conditional join so that an empty segment list
// does not produce a double slash (e.g. '/v1/ext//' for list('/ext/')).
const encodedSegments = segments.map(s => encodeURIComponent(s));
const base = '/v1/ext' + (encodedSegments.length > 0 ? '/' + encodedSegments.join('/') : '');
return base + (trailingSlash ? '/' : '');
}

/** @ignore */
function ExtLibs(client) {
if (!(this instanceof ExtLibs)) {
Expand All @@ -56,11 +99,7 @@ ExtLibs.prototype.read = function readExtensionLibrary(path) {

const requestOptions = mlutil.copyProperties(this.client.getConnectionParams());
requestOptions.method = 'GET';
requestOptions.path = encodeURI(
(path.substr(0,5) === '/ext/') ? ('/v1'+path) :
(path.substr(0,1) === '/') ? ('/v1/ext'+path) :
('/v1/ext/'+path)
);
requestOptions.path = encodeExtPath(path);

const operation = new Operation(
'read extension library', this.client, requestOptions, 'empty', 'single'
Expand Down Expand Up @@ -127,15 +166,13 @@ ExtLibs.prototype.write = function writeExtensionLibrary() {
throw new Error('must specify the path, content type, and source when writing a extension library');
}

let endpoint =
(path.substr(0,5) === '/ext/') ? ('/v1'+path) :
(path.substr(0,1) === '/') ? ('/v1/ext'+path) :
('/v1/ext/'+path);
const encodedPath = encodeExtPath(path);

let queryString = '';
if (Array.isArray(permissions)) {
let role = null;
let capabilities = null;
let j=null;
let j = null;
for (i=0; i < permissions.length; i++) {
arg = permissions[i];
role = arg['role-name'];
Expand All @@ -144,7 +181,8 @@ ExtLibs.prototype.write = function writeExtensionLibrary() {
throw new Error('cannot set permissions from '+JSON.stringify(arg));
}
for (j=0; j < capabilities.length; j++) {
endpoint += ((i === 0 && j=== 0) ? '?' : '&') + 'perm:'+role+'='+capabilities[j];
queryString += ((queryString.length === 0) ? '?' : '&') +
'perm:' + encodeURIComponent(role) + '=' + encodeURIComponent(capabilities[j]);
}
}
}
Expand All @@ -154,7 +192,7 @@ ExtLibs.prototype.write = function writeExtensionLibrary() {
requestOptions.headers = {
'Content-Type': contentType
};
requestOptions.path = encodeURI(endpoint);
requestOptions.path = encodedPath + queryString;

const operation = new Operation(
'write extension library', this.client, requestOptions, 'single', 'empty'
Expand All @@ -180,11 +218,7 @@ ExtLibs.prototype.remove = function removeExtensionLibrary(path) {

const requestOptions = mlutil.copyProperties(this.client.getConnectionParams());
requestOptions.method = 'DELETE';
requestOptions.path = encodeURI(
(path.substr(0,5) === '/ext/') ? ('/v1'+path) :
(path.substr(0,1) === '/') ? ('/v1/ext'+path) :
('/v1/ext/'+path)
);
requestOptions.path = encodeExtPath(path);

const operation = new Operation(
'remove extension library', this.client, requestOptions, 'empty', 'empty'
Expand Down Expand Up @@ -215,24 +249,8 @@ ExtLibs.prototype.list = function listExtensionLibraries(directory) {

if (typeof directory !== 'string' && !(directory instanceof String)) {
requestOptions.path = '/v1/ext';
} else if (directory.substr(0,5) === '/ext/') {
if (directory.substr(-1,1) === '/') {
requestOptions.path = encodeURI(directory);
} else {
requestOptions.path = encodeURI(directory+'/');
}
} else {
const hasInitialSlash = (directory.substr(0,1) === '/');
const hasTrailingSlash = (directory.substr(-1,1) === '/');
if (hasInitialSlash && hasTrailingSlash) {
requestOptions.path = encodeURI('/v1/ext' + directory);
} else if (hasTrailingSlash) {
requestOptions.path = encodeURI('/v1/ext/' + directory);
} else if (hasInitialSlash) {
requestOptions.path = encodeURI('/v1/ext' + directory+'/');
} else {
requestOptions.path = encodeURI('/v1/ext/' + directory+'/');
}
requestOptions.path = encodeExtPath(directory, true);
}

const operation = new Operation(
Expand Down
10 changes: 9 additions & 1 deletion lib/marklogic.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
'use strict';
const http = require('http');
Expand Down Expand Up @@ -716,6 +716,7 @@ function initClient(client, inputParams) {
if(inputParams.authType && inputParams.authType.toString().toLowerCase() === 'cloud' && !isSSL){
isSSL = true;
}

const keys = ['host', 'port', 'database', 'user', 'password', 'authType', 'token', 'basePath','apiKey','accessTokenDuration',
'enableGzippedResponses', 'oauthToken'];
if(isSSL) {
Expand All @@ -738,6 +739,9 @@ function initClient(client, inputParams) {
connectionParams.lookup = prefLookup;

const authType = connectionParams.authType.toUpperCase();
if (authType === 'BASIC' && !isSSL) {
console.warn('Authentication type is BASIC and SSL is not enabled. This may expose credentials.');
}
if ((authType === 'DIGEST' || authType === 'BASIC') &&
((connectionParams.user == null) || (connectionParams.password == null))) {
throw new Error('cannot create client without user or password for '+
Expand All @@ -761,6 +765,10 @@ function initClient(client, inputParams) {
if(!connectionParams.apiKey) {
throw new Error('apiKey needed for MarkLogic cloud authentication.');
}
if (connectionParams.accessTokenDuration !== undefined &&
(!Number.isInteger(connectionParams.accessTokenDuration) || connectionParams.accessTokenDuration <= 0)) {
throw new Error('accessTokenDuration must be a positive integer.');
}
connectionParams.port = 443;
} else if(authType === 'OAUTH' && !connectionParams.oauthToken){
throw new Error('oauthToken required for OAuth authentication. ');
Expand Down
41 changes: 39 additions & 2 deletions lib/plan-builder-base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
'use strict';

Expand Down Expand Up @@ -58,8 +58,18 @@ function castArg(arg, funcName, paramName, argPos, paramTypes) {
} else if (arg instanceof Number || arg instanceof Boolean || arg instanceof String) {
arg = arg.valueOf();
} else if (arg instanceof types.ServerType) {
// We added new VecVector ServerType which is not a sub-type of Item. This makes it a one-off type
// as paramTypes will not include VecVector in any other type checks.
// And if we get here the arg must be and only be a VecVector (arg._ns === 'vec') or we throw an Error
if(arg._ns === 'vec'){
return arg._args;
return arg;
}
// cts.param() is a valid placeholder wherever a cts.query is accepted (e.g. as a direct
// sub-query argument to cts.orQuery, cts.andQuery, cts.notQuery, etc.).
// Serialisation already works: exportObject converts _ns/_fn/_args -> ns/fn/args.
if (arg._ns === 'cts' && arg._fn === 'param' &&
paramTypes.includes(types.CtsQuery)) {
return arg;
}
throw new Error(
`${argLabel(funcName, paramName, argPos)} must have type ${typeLabel(paramTypes)}`
Expand Down Expand Up @@ -133,6 +143,13 @@ function castArg(arg, funcName, paramName, argPos, paramTypes) {
throw new Error(
'bm25LengthWeight must be a number'
);
case 'fragment':
if (['document', 'properties', 'locks', 'any'].includes(value)) {
return true;
}
throw new Error(
`${argLabel(funcName, paramName, argPos)} fragment can only be 'document', 'properties', 'locks', or 'any'`
);
default:
return false;
}});
Expand Down Expand Up @@ -401,6 +418,26 @@ function castArg(arg, funcName, paramName, argPos, paramTypes) {
});
}
return true;
case 'PlanTransitiveClosureOptions':
const planTransitiveClosureOptionsSet = new Set(['minLength', 'min-length', 'maxLength', 'max-length']);
if(Object.getPrototypeOf(arg) === Map.prototype){
arg.forEach((value, key) => {
if(!planTransitiveClosureOptionsSet.has(key)) {
throw new Error(
`${argLabel(funcName, paramName, argPos)} has invalid key- ${key}`
);
}
});
} else if (typeof arg === 'object') {
Object.keys(arg).forEach(key => {
if(!planTransitiveClosureOptionsSet.has(key)) {
throw new Error(
`${argLabel(funcName, paramName, argPos)} has invalid key- ${key}`
);
}
});
}
return true;
default:
return false;
}
Expand Down
Loading
Loading