fix(mongo-adapter): recognize ObjectId and UUID values from a different bson instance

This commit is contained in:
Bereket Engida
2026-09-04 15:07:16 -07:00
parent 5fe5bc21d1
commit d3baa4cf49
3 changed files with 112 additions and 9 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@better-auth/mongo-adapter": patch
---
Recognize ObjectId and UUID values returned by a `mongodb` driver that loaded a different `bson` module instance, so ids are normalized to strings instead of failing `instanceof`.
@@ -1,3 +1,4 @@
import type { BetterAuthOptions } from "@better-auth/core";
import type { Db } from "mongodb";
import { ObjectId, UUID } from "mongodb";
import { describe, expect, it, vi } from "vitest";
@@ -535,3 +536,74 @@ describe("uuid support", () => {
expect((result as Record<string, unknown>).id).toBe(uuid);
});
});
/**
* When the driver and the adapter resolve different `bson` module instances
* (CJS vs ESM entry point), driver values fail `instanceof` against the
* adapter's classes and must be recognized structurally instead.
* @see https://github.com/better-auth/better-auth/issues/11065
*/
describe("ids from a different bson instance", () => {
const hex = "000000000000000000000001";
const uuid = "550e8400-e29b-41d4-a716-446655440000";
const foreignObjectId = { _bsontype: "ObjectId", toHexString: () => hex };
const foreignUUID = {
_bsontype: "Binary",
sub_type: 4,
toHexString: () => uuid,
toString: () => uuid,
};
function createAdapterReturning(
document: Record<string, unknown>,
generateId?: "uuid",
) {
const aggregate = vi.fn(() => ({
toArray: vi.fn(async () => [document]),
}));
const db = { collection: vi.fn(() => ({ aggregate })) } as unknown as Db;
const adapter = mongodbAdapter(db, { transaction: false })({
advanced: { database: generateId ? { generateId } : {} },
} as unknown as BetterAuthOptions);
return { adapter, aggregate };
}
it("normalizes a foreign ObjectId to its hex string", async () => {
const { adapter } = createAdapterReturning({
_id: foreignObjectId,
email: "example@example.test",
});
const user = await adapter.findOne<{ id: unknown }>({
model: "user",
where: [],
});
expect(user?.id).toBe(hex);
});
it("normalizes a foreign UUID to its string form", async () => {
const { adapter } = createAdapterReturning(
{ _id: foreignUUID, email: "example@example.test" },
"uuid",
);
const user = await adapter.findOne<{ id: unknown }>({
model: "user",
where: [],
});
expect(user?.id).toBe(uuid);
});
it("accepts a foreign ObjectId as a where clause value", async () => {
const { adapter, aggregate } = createAdapterReturning({
_id: foreignObjectId,
email: "example@example.test",
});
await adapter.findOne({
model: "user",
where: [{ field: "id", value: foreignObjectId as unknown as string }],
});
expect(aggregate).toHaveBeenCalledWith(
expect.arrayContaining([{ $match: { _id: foreignObjectId } }]),
expect.anything(),
);
});
});
+35 -9
View File
@@ -21,6 +21,32 @@ import {
insensitiveStartsWith,
} from "./query-builders";
/**
* Structural checks for BSON ids. The driver may resolve a different `bson`
* module instance than the adapter (CJS vs ESM entry point), in which case
* its values fail `instanceof` against the adapter's classes.
* @see https://github.com/better-auth/better-auth/issues/11065
*/
function isObjectIdLike(value: unknown): value is ObjectId {
if (typeof value !== "object" || value === null) return false;
const { _bsontype, toHexString } = value as Record<string, unknown>;
return (
(_bsontype === "ObjectId" || _bsontype === "ObjectID") &&
typeof toHexString === "function"
);
}
function isUUIDLike(value: unknown): value is UUID {
if (typeof value !== "object" || value === null) return false;
const { _bsontype, sub_type, toHexString } = value as Record<string, unknown>;
// A BSON UUID is a Binary with sub_type 4.
return (
_bsontype === "Binary" &&
sub_type === 4 &&
typeof toHexString === "function"
);
}
class MongoAdapterError extends Error {
constructor(
public code: "INVALID_ID" | "UNSUPPORTED_OPERATOR",
@@ -157,8 +183,8 @@ export const mongodbAdapter = (
}
function isIdInstance(value: unknown): value is ObjectId | UUID {
if (useUUIDs) return value instanceof UUID;
return value instanceof ObjectId;
if (useUUIDs) return isUUIDLike(value);
return isObjectIdLike(value);
}
function serializeID({
@@ -801,9 +827,9 @@ export const mongodbAdapter = (
if (action !== "create" && action !== "update") {
return data;
}
const IdClass =
options.advanced?.database?.generateId === "uuid" ? UUID : ObjectId;
if (data instanceof IdClass) {
const useUUIDs = options.advanced?.database?.generateId === "uuid";
const IdClass = useUUIDs ? UUID : ObjectId;
if (useUUIDs ? isUUIDLike(data) : isObjectIdLike(data)) {
return data;
}
if (Array.isArray(data)) {
@@ -841,18 +867,18 @@ export const mongodbAdapter = (
},
customTransformOutput({ data, field, fieldAttributes }) {
if (field === "id" || fieldAttributes.references?.field === "id") {
if (data instanceof UUID) {
if (isUUIDLike(data)) {
return data.toString();
}
if (data instanceof ObjectId) {
if (isObjectIdLike(data)) {
return data.toHexString();
}
if (Array.isArray(data)) {
return data.map((v) => {
if (v instanceof UUID) {
if (isUUIDLike(v)) {
return v.toString();
}
if (v instanceof ObjectId) {
if (isObjectIdLike(v)) {
return v.toHexString();
}
return v;