Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f678190
fix issue with scram that impacts deno, by avoiding implicit toString…
PavelSafronov Apr 29, 2026
cc1dea2
remove two more toString calls, replace with ByteUtils.toUTF8 calls
PavelSafronov Apr 30, 2026
b1d102a
fix bundled unit tests
PavelSafronov Apr 30, 2026
ced412f
add test to check for Node properties on non-node sandbox
PavelSafronov May 4, 2026
d0e7fdf
lint
PavelSafronov May 5, 2026
c202e61
remove debug code, add comments
PavelSafronov May 5, 2026
fce3dce
remove debug statement
PavelSafronov May 5, 2026
eeef194
pr feedback:
PavelSafronov May 5, 2026
ddf8f3f
pr feedback:
PavelSafronov May 6, 2026
00e8b36
Merge branch 'main' into node-7548-scram-deno
PavelSafronov May 6, 2026
4ae5259
fix broken nodeless tests
PavelSafronov May 6, 2026
ddf9c51
lint fixes
PavelSafronov May 6, 2026
9ef574a
explicit string conversion in tests
PavelSafronov May 6, 2026
4e62eca
lint
PavelSafronov May 6, 2026
7979bd1
skip some node-only tests
PavelSafronov May 6, 2026
a58ba8d
add a missing type to bundler
PavelSafronov May 6, 2026
e1825c0
tests without metadata now inherit parent metadata
PavelSafronov May 6, 2026
6193546
skip another node test
PavelSafronov May 6, 2026
0b72911
pass string to UUID constructor, so it doesn't throw on a bundled type
PavelSafronov May 7, 2026
49dfe25
pr feedback:
PavelSafronov May 8, 2026
c9b90e5
pr feedback:
PavelSafronov May 11, 2026
b7234d1
pr feedback: added a comment about why we're calling toHexString, lin…
PavelSafronov May 11, 2026
9c5f8ef
Merge branch 'main' into node-7548-scram-deno
tadjik1 May 18, 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
5 changes: 2 additions & 3 deletions etc/bundle-driver.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ await esbuild.build({
entryPoints: [path.join(rootDir, 'test/mongodb_all.ts')],
bundle: true,
outfile: outputBundleFile,
platform: 'node',
platform: 'browser',
format: 'cjs',
target: 'node20',
target: 'chrome112',
external: [
'@aws-sdk/credential-providers',
'@mongodb-js/saslprep',
'@mongodb-js/zstd',
'@napi-rs/snappy*',
'bson',
'gcp-metadata',
'kerberos',
'mongodb-client-encryption',
Expand Down
5 changes: 5 additions & 0 deletions src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export const readInt32LE = (buffer: Uint8Array, offset: number): number => {
return NumberUtils.getInt32LE(buffer, offset);
};

// readUint8, reads a single unsigned byte from buffer at given offset
export const readUint8 = (buffer: Uint8Array, offset: number): number => {
Comment thread
tadjik1 marked this conversation as resolved.
return buffer[offset];
};

export const setUint32LE = (destination: Uint8Array, offset: number, value: number): 4 => {
destination[offset] = value;
value >>>= 8;
Expand Down
11 changes: 5 additions & 6 deletions src/cmap/auth/scram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,10 @@ async function continueScramConversation(
const clientKey = await HMAC(cryptoMethod, saltedPassword, 'Client Key');
const serverKey = await HMAC(cryptoMethod, saltedPassword, 'Server Key');
const storedKey = await H(cryptoMethod, clientKey);
const authMessage = [
clientFirstMessageBare(username, nonce),
payload.toString('utf8'),
withoutProof
].join(',');
const firstMessageBytes = clientFirstMessageBare(username, nonce);
const firstMessage = ByteUtils.toUTF8(firstMessageBytes, 0, firstMessageBytes.length, true);
Comment thread
tadjik1 marked this conversation as resolved.
Outdated
const payloadString = ByteUtils.toUTF8(payload.buffer, 0, payload.buffer.length, true);
Comment thread
tadjik1 marked this conversation as resolved.
Outdated
const authMessage = [firstMessage, payloadString, withoutProof].join(',');
Comment thread
addaleax marked this conversation as resolved.

const clientSignature = await HMAC(cryptoMethod, storedKey, authMessage);
const clientProof = `p=${xor(clientKey, clientSignature)}`;
Expand Down Expand Up @@ -205,7 +204,7 @@ async function continueScramConversation(
}

function parsePayload(payload: Binary) {
const payloadStr = payload.toString('utf8');
const payloadStr = ByteUtils.toUTF8(payload.buffer, 0, payload.buffer.length, true);
Comment thread
tadjik1 marked this conversation as resolved.
Outdated
const dict: Document = {};
const parts = payloadStr.split(',');
for (let i = 0; i < parts.length; i++) {
Expand Down
2 changes: 2 additions & 0 deletions test/mongodb_bundled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const {
decompress,
decorateWithExplain,
DEFAULT_ALLOWED_HOSTS,
DEFAULT_KEEP_ALIVE_INITIAL_DELAY_MS,
DEFAULT_MAX_DOCUMENT_LENGTH,
DEFAULT_PK_FACTORY,
DeleteManyOperation,
Expand Down Expand Up @@ -176,6 +177,7 @@ export const {
ListIndexesOperation,
Long,
makeClientMetadata,
makeSocket,
MAX_SUPPORTED_SERVER_VERSION,
MAX_SUPPORTED_WIRE_VERSION,
MaxKey,
Expand Down
58 changes: 53 additions & 5 deletions test/tools/runner/vm_context_helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ import { isBuiltin } from 'node:module';
import * as path from 'node:path';
import * as vm from 'node:vm';

import * as process from 'process';

import { ALLOWED_DRIVER_REQUIRE_PROPERTY_NAME } from '../../mongodb_all';

/**
* Debug logging for bundled test environment issues
*/
function debug(msg: unknown) {
if (process.env.MONGODB_BUNDLE_DEBUG) {
console.log(`[BUNDLE_DEBUG] ${msg}`);
}
}

const allowedModules = new Set([
'@aws-sdk/credential-providers',
'@mongodb-js/saslprep',
'@mongodb-js/zstd',
'bson',
'gcp-metadata',
'kerberos',
'mongodb-client-encryption',
Expand All @@ -27,7 +37,6 @@ const exposedGlobals = new Set([
'AbortController',
'AbortSignal',
'BigInt',
'Buffer',
'Date',
'Error',
'Headers',
Expand All @@ -42,8 +51,9 @@ const exposedGlobals = new Set([
'console',
'crypto',
'performance',
'process',

'atob',
'btoa',
'clearImmediate',
'clearInterval',
'clearTimeout',
Expand All @@ -68,7 +78,10 @@ function createRestrictedRequire() {
if (shouldBlock) {
throw new Error(`Access to core module '${moduleName}' is restricted in this context`);
}
return require(moduleName);

const required = require(moduleName);
debug(`Loaded external module: ${moduleName}`);
return required;
} as NodeJS.Require;
}

Expand All @@ -80,7 +93,13 @@ const context = {

// Needed for some modules
global: undefined as any,
globalThis: undefined as any
globalThis: undefined as any,

TextEncoder: undefined,
TextDecoder: undefined,

atob: undefined,
btoa: undefined
};

// Expose allowed globals in the context
Expand All @@ -90,12 +109,41 @@ for (const globalName of exposedGlobals) {
}
}

// Ensure TextEncoder/TextDecoder are always available (needed for webByteUtils)
if (!context.TextEncoder && typeof TextEncoder !== 'undefined') {
context.TextEncoder = TextEncoder;
}
if (!context.TextDecoder && typeof TextDecoder !== 'undefined') {
context.TextDecoder = TextDecoder;
}

// Ensure btoa/atob are available (needed for webByteUtils base64 encoding)
if (!context.btoa && typeof btoa !== 'undefined') {
context.btoa = btoa;
}
if (!context.atob && typeof atob !== 'undefined') {
context.atob = atob;
}

// Create a sandbox context with necessary globals
const sandbox = vm.createContext(context);

// Make globalThis point to the sandbox
sandbox.globalThis = sandbox;

export { sandbox };

// Diagnostic: Check if Buffer is accessible in the VM context
if (process.env.MONGODB_BUNDLE_DEBUG) {
try {
const testScript = new vm.Script('typeof Buffer');
const bufferType = testScript.runInContext(sandbox);
debug(`In VM context, typeof Buffer = ${bufferType}`);
} catch (e) {
debug(`Error checking Buffer in context: ${(e as Error).message}`);
}
}

/**
* Load the bundled MongoDB driver module in a VM context
* This allows us to control the globals that the driver has access to
Expand Down
5 changes: 5 additions & 0 deletions test/unit/client-side-encryption/state_machine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Db,
type FindOptions,
MongoClient,
runNodelessTests,
squashError,
StateMachine
} from '../../mongodb';
Expand Down Expand Up @@ -60,6 +61,10 @@ describe('StateMachine', function () {
let clientStub;

beforeEach(function () {
if (runNodelessTests) {
// sinon doesn't work in nodeless tests
this.skip();
}
this.sinon = sinon.createSandbox();
runCommandStub = this.sinon.stub().resolves({});
dbStub = this.sinon.createStubInstance(Db, {
Expand Down
19 changes: 13 additions & 6 deletions test/unit/cmap/commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import * as BSON from 'bson';
import { expect } from 'chai';

// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { readInt32LE } from '../../../src/bson';
import { DocumentSequence, OpMsgRequest, OpReply } from '../../mongodb';

// Helper to decode UTF-8 string from Uint8Array
function utf8Slice(buffer: Uint8Array, start: number, end: number): string {
return BSON.ByteUtils.toUTF8(buffer, start, end, false);
}

describe('commands', function () {
describe('OpMsgRequest', function () {
describe('#toBin', function () {
Expand Down Expand Up @@ -41,12 +48,12 @@ describe('commands', function () {

it('sets the length of the document sequence', function () {
// Bytes starting at index 1 is a 4 byte length.
expect(buffers[3].readInt32LE(1)).to.equal(25);
expect(readInt32LE(buffers[3], 1)).to.equal(25);
});

it('sets the name of the first field to be replaced', function () {
// Bytes starting at index 5 is the field name.
expect(buffers[3].toString('utf8', 5, 10)).to.equal('field');
expect(utf8Slice(buffers[3], 5, 10)).to.equal('field');
});
});

Expand Down Expand Up @@ -81,12 +88,12 @@ describe('commands', function () {

it('sets the length of the first document sequence', function () {
// Bytes starting at index 1 is a 4 byte length.
expect(buffers[3].readInt32LE(1)).to.equal(28);
expect(readInt32LE(buffers[3], 1)).to.equal(28);
});

it('sets the name of the first field to be replaced', function () {
// Bytes starting at index 5 is the field name.
expect(buffers[3].toString('utf8', 5, 13)).to.equal('fieldOne');
expect(utf8Slice(buffers[3], 5, 13)).to.equal('fieldOne');
});

it('sets the document sequence sections second type to 1', function () {
Expand All @@ -96,12 +103,12 @@ describe('commands', function () {

it('sets the length of the second document sequence', function () {
// Bytes starting at index 1 is a 4 byte length.
expect(buffers[3].readInt32LE(30)).to.equal(28);
expect(readInt32LE(buffers[3], 30)).to.equal(28);
});

it('sets the name of the second field to be replaced', function () {
// Bytes starting at index 33 is the field name.
expect(buffers[3].toString('utf8', 34, 42)).to.equal('fieldTwo');
expect(utf8Slice(buffers[3], 34, 42)).to.equal('fieldTwo');
});
});
});
Expand Down
8 changes: 7 additions & 1 deletion test/unit/cmap/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
MongoClientAuthProviders,
MongoCredentials,
MongoNetworkError,
prepareHandshakeDocument
prepareHandshakeDocument,
runNodelessTests
} from '../../mongodb';
import { genClusterTime } from '../../tools/common';
import * as mock from '../../tools/mongodb-mock/index';
Expand Down Expand Up @@ -467,6 +468,11 @@ describe('Connect Tests', function () {
);

before(function (done) {
if (runNodelessTests) {
// sinon doesn't work in nodeless tests
this.skip();
}

// @SECLEVEL=0 allows the legacy test certificate (signed with SHA-1/1024-bit RSA)
// to be accepted by OpenSSL 3.x, which rejects at the default security level.
tlsServer = tls.createServer(
Expand Down
54 changes: 44 additions & 10 deletions test/unit/cmap/wire_protocol/on_demand/document.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Binary, BSON, BSONError, BSONType, ObjectId, Timestamp } from 'bson';
import { expect } from 'chai';

import { OnDemandDocument } from '../../../../mongodb';
import { OnDemandDocument, runNodelessTests } from '../../../../mongodb';
import { ensureTypeByName } from '../../../../tools/utils';

describe('class OnDemandDocument', () => {
context('when given an empty BSON sequence', () => {
Expand Down Expand Up @@ -124,15 +125,32 @@ describe('class OnDemandDocument', () => {
});

it('throws if required is set to true and element name does not exist', () => {
expect(() => document.get('blah!', BSONType.bool, true)).to.throw(BSONError);
if (runNodelessTests) {
try {
document.get('blah!', BSONType.bool, true);
} catch (e) {
ensureTypeByName(e, 'BSONError');
}
} else {
Comment thread
tadjik1 marked this conversation as resolved.
expect(() => document.get('blah!', BSONType.bool, true)).to.throw(BSONError);
}
expect(document).to.have.nested.property('cache.blah!', false);
});

it('throws if requested type is unsupported', () => {
expect(() => {
// @ts-expect-error: checking a bad BSON type
document.get('unsupportedType', BSONType.regex);
}).to.throw(BSONError, /unsupported/i);
if (runNodelessTests) {
try {
// @ts-expect-error: checking a bad BSON type
document.get('unsupportedType', BSONType.regex, true);
} catch (e) {
ensureTypeByName(e, 'BSONError');
}
Comment thread
tadjik1 marked this conversation as resolved.
} else {
expect(() => {
// @ts-expect-error: checking a bad BSON type
document.get('unsupportedType', BSONType.regex);
}).to.throw(BSONError, /unsupported/i);
}
});

it('caches the value', () => {
Expand Down Expand Up @@ -245,15 +263,31 @@ describe('class OnDemandDocument', () => {
});

it('throws if required is set to true and element name does not exist', () => {
expect(() => document.getNumber('blah!', true)).to.throw(BSONError);
if (runNodelessTests) {
try {
document.getNumber('blah!', true);
} catch (e) {
ensureTypeByName(e, 'BSONError');
}
} else {
Comment thread
tadjik1 marked this conversation as resolved.
expect(() => document.getNumber('blah!', true)).to.throw(BSONError);
}
});

it('throws if required is set to true and element is not numeric', () => {
// just making sure this test does not fail for the non-exist reason
expect(document.has('string')).to.be.true;
expect(() => {
document.getNumber('string', true);
}).to.throw(BSONError);
if (runNodelessTests) {
try {
document.getNumber('string', true);
} catch (e) {
ensureTypeByName(e, 'BSONError');
}
} else {
expect(() => {
document.getNumber('string', true);
}).to.throw();
Comment thread
tadjik1 marked this conversation as resolved.
Outdated
}
});

it('returns null if required is set to false and element does not exist', () => {
Expand Down
Loading
Loading