fix: fall back to _id for user id in createSafeUser (#14083)

* fix: fall back to _id for user id in createSafeUser

* chore: reorder imports in env.spec.ts

---------

Co-authored-by: Danny Avila <danacordially@gmail.com>
This commit is contained in:
matt burnett 2026-07-05 08:34:58 -07:00 committed by GitHub
parent f11b7379a1
commit 61e1633b84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 1 deletions

View file

@ -2,7 +2,13 @@ import { Types } from 'mongoose';
import { TokenExchangeMethodEnum } from 'librechat-data-provider';
import type { MCPOptions } from 'librechat-data-provider';
import type { IUser } from '@librechat/data-schemas';
import { resolveHeaders, resolveNestedObject, processMCPEnv, encodeHeaderValue } from './env';
import {
resolveNestedObject,
encodeHeaderValue,
resolveHeaders,
createSafeUser,
processMCPEnv,
} from './env';
function isStdioOptions(options: MCPOptions): options is Extract<MCPOptions, { type?: 'stdio' }> {
return !options.type || options.type === 'stdio';
@ -2124,3 +2130,62 @@ describe('processMCPEnv', () => {
});
});
});
describe('createSafeUser', () => {
it('returns an empty object for null/undefined users', () => {
expect(createSafeUser(null)).toEqual({});
expect(createSafeUser(undefined)).toEqual({});
});
it('falls back to _id (ObjectId) when the virtual id is absent', () => {
const objectId = new Types.ObjectId();
const user = { _id: objectId, email: 'lean@example.com' } as unknown as IUser;
const safeUser = createSafeUser(user);
expect(safeUser.id).toBe(objectId.toString());
expect(safeUser.email).toBe('lean@example.com');
});
it('falls back to a string _id when the virtual id is absent', () => {
const user = { _id: 'string-id-123', email: 'lean@example.com' } as unknown as IUser;
const safeUser = createSafeUser(user);
expect(safeUser.id).toBe('string-id-123');
});
it('leaves a truthy id untouched and does not use _id', () => {
const objectId = new Types.ObjectId();
const user = { _id: objectId, id: 'real-id', email: 'user@example.com' } as unknown as IUser;
const safeUser = createSafeUser(user);
expect(safeUser.id).toBe('real-id');
expect(safeUser.id).not.toBe(objectId.toString());
});
it('replaces a falsy (empty-string) id with _id', () => {
const objectId = new Types.ObjectId();
const user = { _id: objectId, id: '', email: 'user@example.com' } as unknown as IUser;
const safeUser = createSafeUser(user);
expect(safeUser.id).toBe(objectId.toString());
});
it('leaves id undefined when _id is absent', () => {
const user = { email: 'no-id@example.com' } as unknown as IUser;
const safeUser = createSafeUser(user);
expect(safeUser.id).toBeUndefined();
});
it('leaves id undefined when _id is nullish and does not throw', () => {
const user = { _id: null, email: 'null-id@example.com' } as unknown as IUser;
expect(() => createSafeUser(user)).not.toThrow();
expect(createSafeUser(user).id).toBeUndefined();
});
});

View file

@ -104,6 +104,13 @@ export function createSafeUser(
}
}
// Fall back to `_id` when the mongoose virtual `id` is absent (e.g. lean/plain
// user objects), so `{{LIBRECHAT_USER_ID}}` placeholders still resolve.
if (!safeUser.id && '_id' in user) {
const _id = (user as unknown as { _id: { toString?: () => string } | string })._id;
safeUser.id = typeof _id === 'string' ? _id : _id?.toString?.();
}
if ('federatedTokens' in user) {
safeUser.federatedTokens = user.federatedTokens;
}