CVE-2026-39411
Description
LobeHub is a work-and-lifestyle space to find, build, and collaborate with agent teammates that grow with you. Prior to 2.1.48, the webapi authentication layer trusts a client-controlled X-lobe-chat-auth header that is only XOR-obfuscated, not signed or otherwise authenticated. Because the XOR key is hardcoded in the repository, an attacker can forge arbitrary auth payloads and bypass authentication on protected webapi routes. Affected routes include /webapi/chat/[provider], /webapi/models/[provider], /webapi/models/[provider]/pull, and /webapi/create-image/comfyui. This vulnerability is fixed in 2.1.48.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@lobehub/lobehubnpm | < 2.1.48 | 2.1.48 |
Affected products
1Patches
13327b293d66c🔒 fix: remove apiKey fallback in webapi auth to prevent auth bypass (#13535)
24 files changed · +114 −968
apps/cli/src/api/http.ts+1 −22 modified@@ -3,29 +3,9 @@ import { CLI_API_KEY_ENV } from '../constants/auth'; import { resolveServerUrl } from '../settings'; import { log } from '../utils/logger'; -// Must match the server's SECRET_XOR_KEY (src/envs/auth.ts) -const SECRET_XOR_KEY = 'LobeHub · LobeHub'; - -/** - * XOR-obfuscate a payload and encode as Base64. - * The /webapi/* routes require `X-lobe-chat-auth` with this encoding. - */ -function obfuscatePayloadWithXOR(payload: Record<string, any>): string { - const jsonString = JSON.stringify(payload); - const dataBytes = new TextEncoder().encode(jsonString); - const keyBytes = new TextEncoder().encode(SECRET_XOR_KEY); - - const result = new Uint8Array(dataBytes.length); - for (let i = 0; i < dataBytes.length; i++) { - result[i] = dataBytes[i] ^ keyBytes[i % keyBytes.length]; - } - - return btoa(String.fromCharCode(...result)); -} - export interface AuthInfo { accessToken: string; - /** Headers required for /webapi/* endpoints (includes both X-lobe-chat-auth and Oidc-Auth) */ + /** Headers required for /webapi/* endpoints (Oidc-Auth for authentication) */ headers: Record<string, string>; serverUrl: string; } @@ -52,7 +32,6 @@ export async function getAuthInfo(): Promise<AuthInfo> { headers: { 'Content-Type': 'application/json', 'Oidc-Auth': accessToken, - 'X-lobe-chat-auth': obfuscatePayloadWithXOR({}), }, serverUrl, };
apps/cli/src/commands/generate.test.ts+0 −1 modified@@ -61,7 +61,6 @@ describe('generate command', () => { headers: { 'Content-Type': 'application/json', 'Oidc-Auth': 'test-token', - 'X-lobe-chat-auth': 'test-xor-token', }, serverUrl: 'https://app.lobehub.com', });
packages/utils/src/client/xor-obfuscation.test.ts+0 −370 removed@@ -1,370 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { SECRET_XOR_KEY } from '@/envs/auth'; - -import { obfuscatePayloadWithXOR } from './xor-obfuscation'; - -describe('xor-obfuscation', () => { - describe('obfuscatePayloadWithXOR', () => { - it('应该对简单字符串进行混淆并返回Base64字符串', () => { - const payload = 'hello world'; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - - // 验证结果长度大于0 - expect(result.length).toBeGreaterThan(0); - }); - - it('应该对JSON对象进行混淆', () => { - const payload = { name: 'test', value: 123, active: true }; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该对数组进行混淆', () => { - const payload = [1, 2, 3, 'test', { nested: true }]; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该对复杂嵌套对象进行混淆', () => { - const payload = { - user: { - id: 123, - profile: { - name: 'John Doe', - settings: { - theme: 'dark', - notifications: true, - preferences: ['email', 'sms'], - }, - }, - }, - tokens: ['abc123', 'def456'], - metadata: null, - }; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('相同的输入应该产生相同的输出', () => { - const payload = { test: 'consistent' }; - const result1 = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - const result2 = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - expect(result1).toBe(result2); - }); - - it('不同的输入应该产生不同的输出', () => { - const payload1 = { test: 'value1' }; - const payload2 = { test: 'value2' }; - - const result1 = obfuscatePayloadWithXOR(payload1, SECRET_XOR_KEY); - const result2 = obfuscatePayloadWithXOR(payload2, SECRET_XOR_KEY); - - expect(result1).not.toBe(result2); - }); - - it('应该处理包含特殊字符的字符串', () => { - const payload = 'Hello! @#$%^&*()_+-=[]{}|;:,.<>?/~`"\'\\'; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该处理包含Unicode字符的字符串', () => { - const payload = '你好世界 🌍 émojis 日本語 한국어'; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该处理空字符串', () => { - const payload = ''; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该处理空对象', () => { - const payload = {}; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该处理空数组', () => { - const result = obfuscatePayloadWithXOR([], SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该处理null值', () => { - const payload = null; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该处理数字', () => { - const payload = 42; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该处理布尔值', () => { - const payloadTrue = true; - const payloadFalse = false; - - const resultTrue = obfuscatePayloadWithXOR(payloadTrue, SECRET_XOR_KEY); - const resultFalse = obfuscatePayloadWithXOR(payloadFalse, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof resultTrue).toBe('string'); - expect(typeof resultFalse).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(resultTrue)).not.toThrow(); - expect(() => atob(resultFalse)).not.toThrow(); - - // 验证不同布尔值产生不同结果 - expect(resultTrue).not.toBe(resultFalse); - }); - - it('应该处理包含特殊JSON字符的对象', () => { - const payload = { - quotes: '"double quotes"', - singleQuotes: "'single quotes'", - backslash: 'back\\slash', - newline: 'line1\nline2', - tab: 'col1\tcol2', - unicode: '\u0041\u0042\u0043', - }; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该处理很长的字符串', () => { - const payload = 'a'.repeat(10000); - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - - // 验证结果长度合理(Base64编码后长度应该大约是原始长度的4/3) - expect(result.length).toBeGreaterThan(0); - }); - - it('应该产生不同长度输入的不同输出长度', () => { - const shortPayload = 'short'; - const longPayload = 'this is a much longer string that should produce different output'; - - const shortResult = obfuscatePayloadWithXOR(shortPayload, SECRET_XOR_KEY); - const longResult = obfuscatePayloadWithXOR(longPayload, SECRET_XOR_KEY); - - // 较长的输入应该产生较长的输出 - expect(longResult.length).toBeGreaterThan(shortResult.length); - }); - - it('应该验证输出是有效的Base64格式', () => { - const payload = { test: 'base64 validation' }; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证Base64格式的正则表达式 - const base64Regex = /^[\d+/a-z]*={0,2}$/i; - expect(base64Regex.test(result)).toBe(true); - }); - - it('应该处理包含循环引用的对象(通过JSON.stringify处理)', () => { - // JSON.stringify 会抛出错误处理循环引用,但我们测试正常情况 - const payload = { - id: 1, - name: 'test', - nested: { - back: 'reference', - }, - }; - - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - expect(typeof result).toBe('string'); - expect(() => atob(result)).not.toThrow(); - }); - - it('应该对undefined值进行处理', () => { - const payload = undefined; - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - - // 验证返回值是字符串 - expect(typeof result).toBe('string'); - - // 验证返回值是有效的Base64字符串 - expect(() => atob(result)).not.toThrow(); - }); - - it('应该对包含函数的对象进行处理(函数会被JSON.stringify忽略)', () => { - const payload = { - name: 'test', - fn: function () { - return 'test'; - }, - arrow: () => 'arrow', - value: 123, - }; - - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - expect(typeof result).toBe('string'); - expect(() => atob(result)).not.toThrow(); - }); - - it('应该确保XOR操作的确定性', () => { - const payload = 'deterministic test'; - const results: any[] = []; - - // 多次运行相同输入 - for (let i = 0; i < 10; i++) { - results.push(obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY)); - } - - // 所有结果应该相同 - expect(results.every((result) => result === results[0])).toBe(true); - }); - - it('应该处理包含日期对象的数据', () => { - const payload = { - timestamp: new Date('2024-01-01T00:00:00Z'), - created: new Date(), - name: 'date test', - }; - - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - expect(typeof result).toBe('string'); - expect(() => atob(result)).not.toThrow(); - }); - - it('应该处理包含Symbol的对象(Symbol会被JSON.stringify忽略)', () => { - const sym = Symbol('test'); - const payload = { - name: 'symbol test', - [sym]: 'symbol value', - normalKey: 'normal value', - }; - - const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); - expect(typeof result).toBe('string'); - expect(() => atob(result)).not.toThrow(); - }); - - it('应该验证混淆后的数据长度合理性', () => { - const originalPayload = { test: 'length check' }; - const originalJSON = JSON.stringify(originalPayload); - const result = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY); - - // Base64 编码后的长度通常是原始长度的 4/3 倍(向上取整到4的倍数) - const expectedMinLength = Math.ceil((originalJSON.length * 4) / 3 / 4) * 4; - expect(result.length).toBeGreaterThanOrEqual(expectedMinLength - 4); // 允许一些误差 - }); - - it('应该验证XOR操作的正确性(通过逆向操作)', () => { - const originalPayload = { message: 'XOR test', value: 42 }; - const obfuscatedResult = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY); - - // 手动实现逆向操作来验证 XOR 操作的正确性 - const base64Decoded = atob(obfuscatedResult); - const xoredBytes = new Uint8Array(base64Decoded.length); - for (let i = 0; i < base64Decoded.length; i++) { - xoredBytes[i] = base64Decoded.charCodeAt(i); - } - - // 使用相同的密钥进行逆向 XOR 操作 - const keyBytes = new TextEncoder().encode(SECRET_XOR_KEY); - const decodedBytes = new Uint8Array(xoredBytes.length); - for (let i = 0; i < xoredBytes.length; i++) { - decodedBytes[i] = xoredBytes[i] ^ keyBytes[i % keyBytes.length]; - } - - // 将结果转换回字符串 - const decodedString = new TextDecoder().decode(decodedBytes); - const decodedPayload = JSON.parse(decodedString); - - // 验证解码后的数据与原始数据相同 - expect(decodedPayload).toEqual(originalPayload); - }); - - it('应该验证不同输入产生不同的Base64输出', () => { - const payloads = [ - 'test1', - 'test2', - { key: 'value1' }, - { key: 'value2' }, - [1, 2, 3], - [4, 5, 6], - ]; - - const results = payloads.map((payload) => obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY)); - - // 验证所有结果都不相同 - for (let i = 0; i < results.length; i++) { - for (let j = i + 1; j < results.length; j++) { - expect(results[i]).not.toBe(results[j]); - } - } - }); - }); -});
packages/utils/src/client/xor-obfuscation.ts+0 −38 removed@@ -1,38 +0,0 @@ -/** - * Convert string to Uint8Array (UTF-8 encoding) - */ -const stringToUint8Array = (str: string): Uint8Array => { - return new TextEncoder().encode(str); -}; - -/** - * Perform XOR operation on Uint8Array - * @param data The Uint8Array to process - * @param key The key used for XOR operation (Uint8Array) - * @returns The Uint8Array after XOR operation - */ -const xorProcess = (data: Uint8Array, key: Uint8Array): Uint8Array => { - const result = new Uint8Array(data.length); - for (const [i, datum] of data.entries()) { - result[i] = datum ^ key[i % key.length]; // Key is used cyclically - } - return result; -}; - -/** - * Obfuscate payload with XOR and encode to Base64 - * @param payload The JSON object to obfuscate - * @param secretKey The key used for XOR obfuscation - * @returns The obfuscated string encoded in Base64 - */ -export const obfuscatePayloadWithXOR = <T>(payload: T, secretKey: string): string => { - const jsonString = JSON.stringify(payload); - const dataBytes = stringToUint8Array(jsonString); - const keyBytes = stringToUint8Array(secretKey); - - const xoredBytes = xorProcess(dataBytes, keyBytes); - - // Convert Uint8Array to Base64 string - // In browser environment, btoa can only handle Latin-1 characters, so we need to convert to a format suitable for btoa first - return btoa(String.fromCharCode(...xoredBytes)); -};
packages/utils/src/server/index.ts+0 −1 modified@@ -3,4 +3,3 @@ export * from './auth'; export * from './response'; export * from './responsive'; export * from './sse'; -export * from './xor';
packages/utils/src/server/xor.test.ts+0 −123 removed@@ -1,123 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { SECRET_XOR_KEY } from '@/envs/auth'; - -import { obfuscatePayloadWithXOR } from '../client/xor-obfuscation'; -import { getXorPayload } from './xor'; - -describe('getXorPayload', () => { - it('should correctly decode XOR obfuscated payload with user data', () => { - const originalPayload = { - userId: '001362c3-48c5-4635-bd3b-837bfff58fc0', - apiKey: 'test-api-key', - baseURL: 'https://api.example.com', - }; - - // 使用客户端的混淆函数生成token - const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY); - - // 使用服务端的解码函数解码 - const decodedPayload = getXorPayload(obfuscatedToken); - - expect(decodedPayload).toEqual(originalPayload); - }); - - it('should correctly decode XOR obfuscated payload with minimal data', () => { - const originalPayload = { - userId: '12345', - }; - - const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY); - const decodedPayload = getXorPayload(obfuscatedToken); - - expect(decodedPayload).toEqual(originalPayload); - }); - - it('should correctly decode XOR obfuscated payload with AWS credentials', () => { - const originalPayload = { - userId: 'aws-user-123', - awsAccessKeyId: 'AKIAIOSFODNN7EXAMPLE', - awsSecretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', - awsRegion: 'us-east-1', - awsSessionToken: 'session-token-example', - }; - - const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY); - const decodedPayload = getXorPayload(obfuscatedToken); - - expect(decodedPayload).toEqual(originalPayload); - }); - - it('should correctly decode XOR obfuscated payload with Azure data', () => { - const originalPayload = { - userId: 'azure-user-456', - apiKey: 'azure-api-key', - baseURL: 'https://your-resource.openai.azure.com', - azureApiVersion: '2024-02-15-preview', - }; - - const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY); - const decodedPayload = getXorPayload(obfuscatedToken); - - expect(decodedPayload).toEqual(originalPayload); - }); - - it('should correctly decode XOR obfuscated payload with Cloudflare data', () => { - const originalPayload = { - userId: 'cf-user-789', - apiKey: 'cloudflare-api-key', - cloudflareBaseURLOrAccountID: 'account-id-example', - }; - - const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY); - const decodedPayload = getXorPayload(obfuscatedToken); - - expect(decodedPayload).toEqual(originalPayload); - }); - - it('should handle empty payload correctly', () => { - const originalPayload = {}; - - const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY); - const decodedPayload = getXorPayload(obfuscatedToken); - - expect(decodedPayload).toEqual(originalPayload); - }); - - it('should handle payload with undefined values', () => { - const originalPayload = { - userId: 'test-user', - baseURL: undefined, - apiKey: 'test-key', - }; - - const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY); - const decodedPayload = getXorPayload(obfuscatedToken); - - expect(decodedPayload).toEqual(originalPayload); - }); - - it('should throw error for invalid base64 token', () => { - const invalidToken = 'invalid-base64-token!@#'; - - expect(() => getXorPayload(invalidToken)).toThrow(SyntaxError); - }); - - it('should throw error for token that cannot be parsed as JSON', () => { - // 创建一个能正确base64解码但不是有效JSON的token - const invalidJsonString = 'this is not json'; - const invalidJsonBytes = new TextEncoder().encode(invalidJsonString); - const keyBytes = new TextEncoder().encode('LobeHub · LobeHub'); - - // 进行XOR处理 - const result = new Uint8Array(invalidJsonBytes.length); - for (const [i, datum] of invalidJsonBytes.entries()) { - result[i] = datum ^ keyBytes[i % keyBytes.length]; - } - - // 转换为base64 - const invalidToken = Buffer.from(result).toString('base64'); - - expect(() => getXorPayload(invalidToken)).toThrow(SyntaxError); - }); -});
packages/utils/src/server/xor.ts+0 −44 removed@@ -1,44 +0,0 @@ -import type { ClientSecretPayload } from '@lobechat/types'; - -import { SECRET_XOR_KEY } from '@/envs/auth'; - -/** - * Convert Base64 string to Uint8Array - */ -const base64ToUint8Array = (base64: string): Uint8Array => { - // Use Buffer directly in Node.js environment - return Buffer.from(base64, 'base64'); -}; - -/** - * Perform XOR operation on Uint8Array (same as the client-side xorProcess function) - */ -const xorProcess = (data: Uint8Array, key: Uint8Array): Uint8Array => { - const result = new Uint8Array(data.length); - for (const [i, datum] of data.entries()) { - result[i] = datum ^ key[i % key.length]; - } - return result; -}; - -/** - * Convert Uint8Array to string (UTF-8 decoding) - */ -const uint8ArrayToString = (arr: Uint8Array): string => { - return new TextDecoder().decode(arr); -}; - -export const getXorPayload = (token: string): ClientSecretPayload => { - const keyBytes = new TextEncoder().encode(SECRET_XOR_KEY); - - // 1. Base64 decoding - const base64DecodedBytes = base64ToUint8Array(token); - - // 2. XOR deobfuscation - const xorDecryptedBytes = xorProcess(base64DecodedBytes, keyBytes); - - // 3. Convert to string and parse JSON - const decodedJsonString = uint8ArrayToString(xorDecryptedBytes); - - return JSON.parse(decodedJsonString) as ClientSecretPayload; -};
src/app/(backend)/middleware/auth/index.test.ts+1 −40 modified@@ -1,9 +1,7 @@ import { AgentRuntimeError } from '@lobechat/model-runtime'; import { ChatErrorType } from '@lobechat/types'; -import { getXorPayload } from '@lobechat/utils/server'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type * as EnvsAuthModule from '@/envs/auth'; import { createErrorResponse } from '@/utils/errorResponse'; import { type RequestHandler } from './index'; @@ -18,17 +16,6 @@ vi.mock('./utils', () => ({ checkAuthMethod: vi.fn(), })); -vi.mock('@lobechat/utils/server', () => ({ - getXorPayload: vi.fn(), -})); - -vi.mock('@/envs/auth', async (importOriginal) => { - const actual = await importOriginal<typeof EnvsAuthModule>(); - return { - ...actual, - }; -}); - vi.mock('@/auth', () => ({ auth: { api: { @@ -50,34 +37,8 @@ describe('checkAuth', () => { vi.resetAllMocks(); }); - it('should return unauthorized error if no authorization header', async () => { - await checkAuth(mockHandler)(mockRequest, mockOptions); - - expect(createErrorResponse).toHaveBeenCalledWith(ChatErrorType.Unauthorized, { - error: AgentRuntimeError.createError(ChatErrorType.Unauthorized), - provider: 'mock', - }); - expect(mockHandler).not.toHaveBeenCalled(); - }); - - it('should return error response on getJWTPayload error', async () => { - const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized); - mockRequest.headers.set('Authorization', 'invalid'); - vi.mocked(getXorPayload).mockRejectedValueOnce(mockError); - - await checkAuth(mockHandler)(mockRequest, mockOptions); - - expect(createErrorResponse).toHaveBeenCalledWith(ChatErrorType.Unauthorized, { - error: mockError, - provider: 'mock', - }); - expect(mockHandler).not.toHaveBeenCalled(); - }); - - it('should return error response on checkAuthMethod error', async () => { + it('should return error response on checkAuthMethod error (no session)', async () => { const mockError = AgentRuntimeError.createError(ChatErrorType.Unauthorized); - mockRequest.headers.set('Authorization', 'valid'); - vi.mocked(getXorPayload).mockResolvedValueOnce({}); vi.mocked(checkAuthMethod).mockImplementationOnce(() => { throw mockError; });
src/app/(backend)/middleware/auth/index.ts+17 −36 modified@@ -1,22 +1,18 @@ -import { type ChatCompletionErrorPayload, type ModelRuntime } from '@lobechat/model-runtime'; +import { type ChatCompletionErrorPayload } from '@lobechat/model-runtime'; import { AgentRuntimeError } from '@lobechat/model-runtime'; import { context as otContext } from '@lobechat/observability-otel/api'; import { type ClientSecretPayload } from '@lobechat/types'; import { ChatErrorType } from '@lobechat/types'; -import { getXorPayload } from '@lobechat/utils/server'; import { auth } from '@/auth'; import { getServerDB } from '@/database/core/db-adaptor'; import { type LobeChatDatabase } from '@/database/type'; -import { LOBE_CHAT_AUTH_HEADER, LOBE_CHAT_OIDC_AUTH_HEADER } from '@/envs/auth'; +import { LOBE_CHAT_OIDC_AUTH_HEADER } from '@/envs/auth'; import { extractTraceContext, injectActiveTraceHeaders } from '@/libs/observability/traceparent'; import { validateOIDCJWT } from '@/libs/oidc-provider/jwt'; import { createErrorResponse } from '@/utils/errorResponse'; -import { checkAuthMethod } from './utils'; - -type CreateRuntime = (jwtPayload: ClientSecretPayload) => ModelRuntime; -type RequestOptions = { createRuntime?: CreateRuntime; params: Promise<{ provider?: string }> }; +type RequestOptions = { params: Promise<{ provider?: string }> }; export type RequestHandler = ( req: Request, @@ -48,41 +44,26 @@ export const checkAuth = }); } - let jwtPayload: ClientSecretPayload; + let userId: string; try { - // get Authorization from header - const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER); - - // better auth handler - const session = await auth.api.getSession({ - headers: req.headers, - }); - - const betterAuthAuthorized = !!session?.user?.id; - - if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized); - - jwtPayload = getXorPayload(authorization); - + // OIDC authentication (CLI) const oidcAuthorization = req.headers.get(LOBE_CHAT_OIDC_AUTH_HEADER); - let isUseOidcAuth = false; - if (!!oidcAuthorization) { + if (oidcAuthorization) { const oidc = await validateOIDCJWT(oidcAuthorization); + userId = oidc.userId; + } else { + // Better Auth session authentication (web) + const session = await auth.api.getSession({ + headers: req.headers, + }); - isUseOidcAuth = true; + if (!session?.user?.id) { + throw AgentRuntimeError.createError(ChatErrorType.Unauthorized); + } - jwtPayload = { - ...jwtPayload, - userId: oidc.userId, - }; + userId = session.user.id; } - - if (!isUseOidcAuth) - checkAuthMethod({ - apiKey: jwtPayload.apiKey, - betterAuthAuthorized, - }); } catch (e) { const params = await options.params; @@ -110,7 +91,7 @@ export const checkAuth = return createErrorResponse(errorType, { error, ...res, provider: params?.provider }); } - const userId = jwtPayload.userId || ''; + const jwtPayload: ClientSecretPayload = { userId }; const extractedContext = extractTraceContext(req.headers);
src/app/(backend)/middleware/auth/utils.test.ts+1 −9 modified@@ -15,19 +15,11 @@ describe('checkAuthMethod', () => { ).not.toThrow(); }); - it('should pass with valid API key', () => { - expect(() => - checkAuthMethod({ - apiKey: 'someApiKey', - }), - ).not.toThrow(); - }); - it('should throw Unauthorized with no auth params', () => { expect(() => checkAuthMethod({})).toThrow(); }); - it('should throw Unauthorized when betterAuthAuthorized is false and no apiKey', () => { + it('should throw Unauthorized when betterAuthAuthorized is false', () => { expect(() => checkAuthMethod({ betterAuthAuthorized: false,
src/app/(backend)/middleware/auth/utils.ts+4 −12 modified@@ -2,25 +2,17 @@ import { AgentRuntimeError } from '@lobechat/model-runtime'; import { ChatErrorType } from '@lobechat/types'; interface CheckAuthParams { - apiKey?: string; betterAuthAuthorized?: boolean; } + /** - * Check if authentication is valid based on various auth methods. - * - * @param {CheckAuthParams} params - Authentication parameters extracted from headers. - * @param {string} [params.apiKey] - The user API key. - * @param {boolean} [params.betterAuthAuthorized] - Whether the Better Auth session exists. - * @throws {AgentRuntimeError} If no valid authentication method is found. + * Check if authentication is valid. + * Only accepts a verified server-side session (Better Auth). */ export const checkAuthMethod = (params: CheckAuthParams) => { - const { apiKey, betterAuthAuthorized } = params; + const { betterAuthAuthorized } = params; - // if better auth session exists if (betterAuthAuthorized) return; - // if apiKey exist - if (apiKey) return; - throw AgentRuntimeError.createError(ChatErrorType.Unauthorized); };
src/app/(backend)/webapi/chat/[provider]/route.test.ts+13 −75 modified@@ -2,11 +2,9 @@ import { type LobeRuntimeAI } from '@lobechat/model-runtime'; import { ModelRuntime } from '@lobechat/model-runtime'; import { ChatErrorType } from '@lobechat/types'; -import { getXorPayload } from '@lobechat/utils/server'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type * as EnvsAuthModule from '@/envs/auth'; -import { LOBE_CHAT_AUTH_HEADER } from '@/envs/auth'; +import { auth } from '@/auth'; import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime'; import { POST } from './route'; @@ -15,22 +13,11 @@ vi.mock('@/app/(backend)/middleware/auth/utils', () => ({ checkAuthMethod: vi.fn(), })); -vi.mock('@lobechat/utils/server', () => ({ - getXorPayload: vi.fn(), -})); - vi.mock('@/server/modules/ModelRuntime', () => ({ initModelRuntimeFromDB: vi.fn(), createTraceOptions: vi.fn().mockReturnValue({}), })); -vi.mock('@/envs/auth', async (importOriginal) => { - const actual = await importOriginal<typeof EnvsAuthModule>(); - return { - ...actual, - }; -}); - vi.mock('@/auth', () => ({ auth: { api: { @@ -43,31 +30,26 @@ vi.mock('@/auth', () => ({ let request: Request; beforeEach(() => { request = new Request(new URL('https://test.com'), { - headers: { - [LOBE_CHAT_AUTH_HEADER]: 'Bearer some-valid-token', - }, method: 'POST', body: JSON.stringify({ model: 'test-model' }), }); + + // Default: valid session + vi.mocked(auth.api.getSession).mockResolvedValue({ + session: {} as any, + user: { id: 'test-user-id' } as any, + }); }); afterEach(() => { - // 清除模拟调用历史 vi.clearAllMocks(); }); describe('POST handler', () => { describe('init chat model', () => { - it('should initialize ModelRuntime correctly with valid authorization', async () => { + it('should initialize ModelRuntime correctly with valid session', async () => { const mockParams = Promise.resolve({ provider: 'test-provider' }); - // 设置 getJWTPayload 的模拟返回值 - vi.mocked(getXorPayload).mockReturnValueOnce({ - apiKey: 'test-api-key', - azureApiVersion: 'v1', - }); - - // chat mock 需要返回一个 Response 对象,否则中间件访问 res.headers 会报错 const mockChatResponse = new Response(JSON.stringify({ success: true }), { headers: { 'Content-Type': 'application/json' }, }); @@ -76,71 +58,33 @@ describe('POST handler', () => { chat: vi.fn().mockResolvedValue(mockChatResponse), }; - // Mock initModelRuntimeFromDB vi.mocked(initModelRuntimeFromDB).mockResolvedValue(new ModelRuntime(mockRuntime)); - // 调用 POST 函数 await POST(request as unknown as Request, { params: mockParams }); - // 验证是否正确调用了模拟函数 - expect(getXorPayload).toHaveBeenCalledWith('Bearer some-valid-token'); expect(initModelRuntimeFromDB).toHaveBeenCalledWith( expect.anything(), - expect.any(String), + 'test-user-id', 'test-provider', ); }); - it('should return Unauthorized error when LOBE_CHAT_AUTH_HEADER is missing', async () => { - const mockParams = Promise.resolve({ provider: 'test-provider' }); - const requestWithoutAuthHeader = new Request(new URL('https://test.com'), { - method: 'POST', - body: JSON.stringify({ model: 'test-model' }), - }); - - const response = await POST(requestWithoutAuthHeader, { params: mockParams }); - - expect(response.status).toBe(401); - expect(await response.json()).toEqual({ - body: { - error: { errorType: 401 }, - provider: 'test-provider', - }, - errorType: 401, - }); - }); + it('should return Unauthorized error when no session exists', async () => { + vi.mocked(auth.api.getSession).mockResolvedValue(null); - it('should return InternalServerError error when throw a unknown error', async () => { const mockParams = Promise.resolve({ provider: 'test-provider' }); - vi.mocked(getXorPayload).mockImplementationOnce(() => { - throw new Error('unknown error'); - }); const response = await POST(request, { params: mockParams }); - expect(response.status).toBe(500); - expect(await response.json()).toEqual({ - body: { - error: {}, - provider: 'test-provider', - }, - errorType: 500, - }); + expect(response.status).toBe(401); }); }); describe('chat', () => { it('should correctly handle chat completion with valid payload', async () => { - vi.mocked(getXorPayload).mockReturnValueOnce({ - apiKey: 'test-api-key', - azureApiVersion: 'v1', - userId: 'abc', - }); - const mockParams = Promise.resolve({ provider: 'test-provider' }); const mockChatPayload = { message: 'Hello, world!' }; request = new Request(new URL('https://test.com'), { - headers: { [LOBE_CHAT_AUTH_HEADER]: 'Bearer some-valid-token' }, method: 'POST', body: JSON.stringify(mockChatPayload), }); @@ -157,21 +101,15 @@ describe('POST handler', () => { expect(response).toEqual(mockChatResponse); expect(mockRuntime.chat).toHaveBeenCalledWith(mockChatPayload, { - user: expect.any(String), + user: 'test-user-id', signal: expect.anything(), }); }); it('should return an error response when chat completion fails', async () => { - vi.mocked(getXorPayload).mockReturnValueOnce({ - apiKey: 'test-api-key', - azureApiVersion: 'v1', - }); - const mockParams = Promise.resolve({ provider: 'test-provider' }); const mockChatPayload = { message: 'Hello, world!' }; request = new Request(new URL('https://test.com'), { - headers: { [LOBE_CHAT_AUTH_HEADER]: 'Bearer some-valid-token' }, method: 'POST', body: JSON.stringify(mockChatPayload), });
src/app/(backend)/webapi/chat/[provider]/route.ts+36 −45 modified@@ -1,4 +1,4 @@ -import { type ChatCompletionErrorPayload, type ModelRuntime } from '@lobechat/model-runtime'; +import { type ChatCompletionErrorPayload } from '@lobechat/model-runtime'; import { AGENT_RUNTIME_ERROR_SET } from '@lobechat/model-runtime'; import { ChatErrorType } from '@lobechat/types'; @@ -12,53 +12,44 @@ import { getTracePayload } from '@/utils/trace'; // this enforce user to enable fluid compute export const maxDuration = 300; -export const POST = checkAuth( - async (req: Request, { params, userId, serverDB, createRuntime, jwtPayload }) => { - const provider = (await params)!.provider!; +export const POST = checkAuth(async (req: Request, { params, userId, serverDB }) => { + const provider = (await params)!.provider!; - try { - // ============ 1. init chat model ============ // - let modelRuntime: ModelRuntime; - if (createRuntime) { - // Legacy support for custom runtime creation - modelRuntime = createRuntime(jwtPayload); - } else { - // Read user's provider config from database - modelRuntime = await initModelRuntimeFromDB(serverDB, userId, provider); - } + try { + // ============ 1. init chat model ============ // + const modelRuntime = await initModelRuntimeFromDB(serverDB, userId, provider); - // ============ 2. create chat completion ============ // + // ============ 2. create chat completion ============ // - const data = (await req.json()) as ChatStreamPayload; + const data = (await req.json()) as ChatStreamPayload; - const tracePayload = getTracePayload(req); + const tracePayload = getTracePayload(req); - let traceOptions = {}; - // If user enable trace - if (tracePayload?.enabled) { - traceOptions = createTraceOptions(data, { provider, trace: tracePayload }); - } - - return await modelRuntime.chat(data, { - user: userId, - ...traceOptions, - signal: req.signal, - }); - } catch (e) { - const { - errorType = ChatErrorType.InternalServerError, - error: errorContent, - ...res - } = e as ChatCompletionErrorPayload; - - const error = errorContent || e; - - const logMethod = AGENT_RUNTIME_ERROR_SET.has(errorType as string) ? 'warn' : 'error'; - // track the error at server side - // eslint-disable-next-line no-console - console[logMethod](`Route: [${provider}] ${errorType}:`, error); - - return createErrorResponse(errorType, { error, ...res, provider }); + let traceOptions = {}; + // If user enable trace + if (tracePayload?.enabled) { + traceOptions = createTraceOptions(data, { provider, trace: tracePayload }); } - }, -); + + return await modelRuntime.chat(data, { + user: userId, + ...traceOptions, + signal: req.signal, + }); + } catch (e) { + const { + errorType = ChatErrorType.InternalServerError, + error: errorContent, + ...res + } = e as ChatCompletionErrorPayload; + + const error = errorContent || e; + + const logMethod = AGENT_RUNTIME_ERROR_SET.has(errorType as string) ? 'warn' : 'error'; + // track the error at server side + // eslint-disable-next-line no-console + console[logMethod](`Route: [${provider}] ${errorType}:`, error); + + return createErrorResponse(errorType, { error, ...res, provider }); + } +});
src/app/(backend)/webapi/models/[provider]/route.test.ts+7 −46 modified@@ -2,11 +2,9 @@ import { type LobeRuntimeAI } from '@lobechat/model-runtime'; import { ModelRuntime } from '@lobechat/model-runtime'; import { ChatErrorType } from '@lobechat/types'; -import { getXorPayload } from '@lobechat/utils/server'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type * as EnvsAuthModule from '@/envs/auth'; -import { LOBE_CHAT_AUTH_HEADER } from '@/envs/auth'; +import { auth } from '@/auth'; import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime'; import { GET } from './route'; @@ -15,17 +13,6 @@ vi.mock('@/app/(backend)/middleware/auth/utils', () => ({ checkAuthMethod: vi.fn(), })); -vi.mock('@lobechat/utils/server', () => ({ - getXorPayload: vi.fn(), -})); - -vi.mock('@/envs/auth', async (importOriginal) => { - const actual = await importOriginal<typeof EnvsAuthModule>(); - return { - ...actual, - }; -}); - vi.mock('@/auth', () => ({ auth: { api: { @@ -42,11 +29,14 @@ let request: Request; beforeEach(() => { request = new Request(new URL('https://test.com'), { - headers: { - [LOBE_CHAT_AUTH_HEADER]: 'Bearer some-valid-token', - }, method: 'GET', }); + + // Default: valid session + vi.mocked(auth.api.getSession).mockResolvedValue({ + session: {} as any, + user: { id: 'test-user-id' } as any, + }); }); afterEach(() => { @@ -58,10 +48,6 @@ describe('GET handler', () => { it('should not expose stack trace when an Error is thrown', async () => { const mockParams = Promise.resolve({ provider: 'google' }); - vi.mocked(getXorPayload).mockReturnValueOnce({ - apiKey: 'test-api-key', - }); - const errorWithStack = new Error('Something went wrong'); errorWithStack.stack = 'Error: Something went wrong\n at Object.<anonymous> (/path/to/file.ts:10:15)'; @@ -76,14 +62,10 @@ describe('GET handler', () => { const response = await GET(request, { params: mockParams }); const responseBody = await response.json(); - // Should contain error name and message expect(responseBody.body.error.name).toBe('Error'); expect(responseBody.body.error.message).toBe('Something went wrong'); - - // Should NOT contain stack trace expect(responseBody.body.error.stack).toBeUndefined(); - // Verify JSON stringified response doesn't contain stack const responseText = JSON.stringify(responseBody); expect(responseText).not.toContain('/path/to/file.ts'); expect(responseText).not.toContain('at Object'); @@ -92,10 +74,6 @@ describe('GET handler', () => { it('should preserve error name for custom error types', async () => { const mockParams = Promise.resolve({ provider: 'google' }); - vi.mocked(getXorPayload).mockReturnValueOnce({ - apiKey: 'test-api-key', - }); - class CustomError extends Error { constructor(message: string) { super(message); @@ -124,10 +102,6 @@ describe('GET handler', () => { it('should pass through structured error objects as-is', async () => { const mockParams = Promise.resolve({ provider: 'google' }); - vi.mocked(getXorPayload).mockReturnValueOnce({ - apiKey: 'test-api-key', - }); - const structuredError = { errorType: ChatErrorType.InternalServerError, error: { code: 'PROVIDER_ERROR', details: 'API limit exceeded' }, @@ -143,18 +117,13 @@ describe('GET handler', () => { const response = await GET(request, { params: mockParams }); const responseBody = await response.json(); - // Structured error should be passed through expect(responseBody.body.error.code).toBe('PROVIDER_ERROR'); expect(responseBody.body.error.details).toBe('API limit exceeded'); }); it('should return correct status code for errors', async () => { const mockParams = Promise.resolve({ provider: 'google' }); - vi.mocked(getXorPayload).mockReturnValueOnce({ - apiKey: 'test-api-key', - }); - const mockRuntime: LobeRuntimeAI = { baseURL: 'abc', chat: vi.fn(), @@ -170,10 +139,6 @@ describe('GET handler', () => { it('should include provider in error response', async () => { const mockParams = Promise.resolve({ provider: 'openai' }); - vi.mocked(getXorPayload).mockReturnValueOnce({ - apiKey: 'test-api-key', - }); - const mockRuntime: LobeRuntimeAI = { baseURL: 'abc', chat: vi.fn(), @@ -192,10 +157,6 @@ describe('GET handler', () => { it('should return model list on success', async () => { const mockParams = Promise.resolve({ provider: 'openai' }); - vi.mocked(getXorPayload).mockReturnValueOnce({ - apiKey: 'test-api-key', - }); - const mockModelList = [ { id: 'gpt-4', name: 'GPT-4' }, { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' },
src/envs/auth.ts+0 −1 modified@@ -298,4 +298,3 @@ export const authEnv = getAuthConfig(); // Auth headers and constants export const LOBE_CHAT_AUTH_HEADER = 'X-lobe-chat-auth'; export const LOBE_CHAT_OIDC_AUTH_HEADER = 'Oidc-Auth'; -export const SECRET_XOR_KEY = 'LobeHub · LobeHub';
src/libs/trpc/lambda/context.test.ts+0 −11 modified@@ -71,7 +71,6 @@ describe('createContextInner', () => { const context = await createContextInner(); expect(context).toMatchObject({ - authorizationHeader: undefined, marketAccessToken: undefined, oidcAuth: undefined, userAgent: undefined, @@ -86,14 +85,6 @@ describe('createContextInner', () => { expect(context.userId).toBe('user-123'); }); - it('should create context with authorization header', async () => { - const context = await createContextInner({ - authorizationHeader: 'Bearer token-abc', - }); - - expect(context.authorizationHeader).toBe('Bearer token-abc'); - }); - it('should create context with user agent', async () => { const context = await createContextInner({ userAgent: 'Mozilla/5.0', @@ -123,7 +114,6 @@ describe('createContextInner', () => { it('should create context with all parameters combined', async () => { const params = { - authorizationHeader: 'Bearer token', userId: 'user-123', userAgent: 'Test Agent', marketAccessToken: 'mp-token', @@ -136,7 +126,6 @@ describe('createContextInner', () => { const context = await createContextInner(params); expect(context).toMatchObject({ - authorizationHeader: 'Bearer token', userId: 'user-123', userAgent: 'Test Agent', marketAccessToken: 'mp-token',
src/libs/trpc/lambda/context.ts+1 −8 modified@@ -7,7 +7,7 @@ import { type NextRequest } from 'next/server'; import { auth } from '@/auth'; import { getServerDB } from '@/database/core/db-adaptor'; import { ApiKeyModel } from '@/database/models/apiKey'; -import { authEnv, LOBE_CHAT_AUTH_HEADER, LOBE_CHAT_OIDC_AUTH_HEADER } from '@/envs/auth'; +import { authEnv, LOBE_CHAT_OIDC_AUTH_HEADER } from '@/envs/auth'; import { extractTraceContext } from '@/libs/observability/traceparent'; import { validateOIDCJWT } from '@/libs/oidc-provider/jwt'; import { isApiKeyExpired, validateApiKeyFormat } from '@/utils/apiKey'; @@ -64,7 +64,6 @@ export interface OIDCAuth { } export interface AuthContext { - authorizationHeader?: string | null; clientIp?: string | null; jwtPayload?: ClientSecretPayload | null; marketAccessToken?: string; @@ -81,7 +80,6 @@ export interface AuthContext { * This is useful for testing when we don't want to mock Next.js' request/response */ export const createContextInner = async (params?: { - authorizationHeader?: string | null; clientIp?: string | null; marketAccessToken?: string; oidcAuth?: OIDCAuth | null; @@ -93,7 +91,6 @@ export const createContextInner = async (params?: { const responseHeaders = new Headers(); return { - authorizationHeader: params?.authorizationHeader, clientIp: params?.clientIp, marketAccessToken: params?.marketAccessToken, oidcAuth: params?.oidcAuth, @@ -118,15 +115,13 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC if (process.env.NODE_ENV === 'development' && (isDebugApi || isMockUser)) { return createContextInner({ - authorizationHeader: request.headers.get(LOBE_CHAT_AUTH_HEADER), userId: process.env.MOCK_DEV_USER_ID, }); } log('createLambdaContext called for request'); // for API-response caching see https://trpc.io/docs/v11/caching - const authorization = request.headers.get(LOBE_CHAT_AUTH_HEADER); const userAgent = request.headers.get('user-agent') || undefined; const clientIp = extractClientIp(request); @@ -139,12 +134,10 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC log('marketAccessToken from cookie:', marketAccessToken ? '[HIDDEN]' : 'undefined'); const commonContext = { - authorizationHeader: authorization, clientIp, marketAccessToken, userAgent, }; - log('LobeChat Authorization header: %s', authorization ? 'exists' : 'not found'); const apiKeyToken = request.headers.get(LOBE_CHAT_API_KEY_HEADER)?.trim(); log('X-API-Key header: %s', apiKeyToken ? 'exists' : 'not found');
src/libs/trpc/lambda/middleware/index.ts+0 −1 modified@@ -1,4 +1,3 @@ -export * from './keyVaults'; export * from './marketSDK'; export * from './marketUserInfo'; export * from './serverDatabase';
src/libs/trpc/lambda/middleware/keyVaults.ts+0 −18 removed@@ -1,18 +0,0 @@ -import { getXorPayload } from '@lobechat/utils/server'; -import { TRPCError } from '@trpc/server'; - -import { trpc } from '../init'; - -export const keyVaults = trpc.middleware(async (opts) => { - const { ctx } = opts; - - if (!ctx.authorizationHeader) throw new TRPCError({ code: 'UNAUTHORIZED' }); - - try { - const jwtPayload = getXorPayload(ctx.authorizationHeader); - - return opts.next({ ctx: { jwtPayload } }); - } catch (e) { - throw new TRPCError({ code: 'UNAUTHORIZED', message: (e as Error).message }); - } -});
src/server/routers/lambda/chunk.ts+16 −19 modified@@ -14,31 +14,28 @@ import { FileModel } from '@/database/models/file'; import { MessageModel } from '@/database/models/message'; import { knowledgeBaseFiles } from '@/database/schemas'; import { authedProcedure, router } from '@/libs/trpc/lambda'; -import { keyVaults, serverDatabase } from '@/libs/trpc/lambda/middleware'; +import { serverDatabase } from '@/libs/trpc/lambda/middleware'; import { getServerDefaultFilesConfig } from '@/server/globalConfig'; import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime'; import { ChunkService } from '@/server/services/chunk'; import { DocumentService } from '@/server/services/document'; -const chunkProcedure = authedProcedure - .use(serverDatabase) - .use(keyVaults) - .use(async (opts) => { - const { ctx } = opts; - - return opts.next({ - ctx: { - asyncTaskModel: new AsyncTaskModel(ctx.serverDB, ctx.userId), - chunkModel: new ChunkModel(ctx.serverDB, ctx.userId), - chunkService: new ChunkService(ctx.serverDB, ctx.userId), - documentModel: new DocumentModel(ctx.serverDB, ctx.userId), - documentService: new DocumentService(ctx.serverDB, ctx.userId), - embeddingModel: new EmbeddingModel(ctx.serverDB, ctx.userId), - fileModel: new FileModel(ctx.serverDB, ctx.userId), - messageModel: new MessageModel(ctx.serverDB, ctx.userId), - }, - }); +const chunkProcedure = authedProcedure.use(serverDatabase).use(async (opts) => { + const { ctx } = opts; + + return opts.next({ + ctx: { + asyncTaskModel: new AsyncTaskModel(ctx.serverDB, ctx.userId), + chunkModel: new ChunkModel(ctx.serverDB, ctx.userId), + chunkService: new ChunkService(ctx.serverDB, ctx.userId), + documentModel: new DocumentModel(ctx.serverDB, ctx.userId), + documentService: new DocumentService(ctx.serverDB, ctx.userId), + embeddingModel: new EmbeddingModel(ctx.serverDB, ctx.userId), + fileModel: new FileModel(ctx.serverDB, ctx.userId), + messageModel: new MessageModel(ctx.serverDB, ctx.userId), + }, }); +}); /** * Group chunks by file and calculate relevance scores
src/server/routers/lambda/ragEval.ts+14 −17 modified@@ -24,27 +24,24 @@ import { EvaluationRecordModel, } from '@/database/models/ragEval'; import { authedProcedure, router } from '@/libs/trpc/lambda'; -import { keyVaults, serverDatabase } from '@/libs/trpc/lambda/middleware'; +import { serverDatabase } from '@/libs/trpc/lambda/middleware'; import { createAsyncCaller } from '@/server/routers/async'; import { FileService } from '@/server/services/file'; -const ragEvalProcedure = authedProcedure - .use(serverDatabase) - .use(keyVaults) - .use(async (opts) => { - const { ctx } = opts; - - return opts.next({ - ctx: { - datasetModel: new EvalDatasetModel(ctx.serverDB, ctx.userId), - fileModel: new FileModel(ctx.serverDB, ctx.userId), - datasetRecordModel: new EvalDatasetRecordModel(ctx.serverDB, ctx.userId), - evaluationModel: new EvalEvaluationModel(ctx.serverDB, ctx.userId), - evaluationRecordModel: new EvaluationRecordModel(ctx.serverDB, ctx.userId), - fileService: new FileService(ctx.serverDB, ctx.userId), - }, - }); +const ragEvalProcedure = authedProcedure.use(serverDatabase).use(async (opts) => { + const { ctx } = opts; + + return opts.next({ + ctx: { + datasetModel: new EvalDatasetModel(ctx.serverDB, ctx.userId), + fileModel: new FileModel(ctx.serverDB, ctx.userId), + datasetRecordModel: new EvalDatasetRecordModel(ctx.serverDB, ctx.userId), + evaluationModel: new EvalEvaluationModel(ctx.serverDB, ctx.userId), + evaluationRecordModel: new EvaluationRecordModel(ctx.serverDB, ctx.userId), + fileService: new FileService(ctx.serverDB, ctx.userId), + }, }); +}); export const ragEvalRouter = router({ createDataset: ragEvalProcedure
src/server/routers/lambda/userMemories.test.ts+1 −1 modified@@ -37,7 +37,7 @@ vi.mock('@/database/models/userMemory', async (importOriginal) => { }); const embeddingsMock = vi.fn(); -const mockCtx = { authorizationHeader: 'Bearer mock-token', userId: 'test-user' }; +const mockCtx = { userId: 'test-user' }; const makeServerDBMock = (query: Record<string, any> = {}) => ({ query: { userSettings: {
src/server/routers/tools/search.test.ts+1 −4 modified@@ -7,10 +7,7 @@ import { SearXNGClient } from '@/server/services/search/impls/searxng/client'; import { searchRouter } from './search'; -// Mock JWT verification -vi.mock('@lobechat/utils/server', () => ({ - getXorPayload: vi.fn().mockReturnValue({ userId: '1' }), -})); +// Mock removed: XOR payload is no longer used for authentication vi.mock('@lobechat/web-crawler', () => ({ Crawler: vi.fn().mockImplementation(() => ({
src/services/_auth.ts+1 −26 modified@@ -1,7 +1,6 @@ import { type AWSBedrockKeyVault, type AzureOpenAIKeyVault, - type ClientSecretPayload, type CloudflareKeyVault, type ComfyUIKeyVault, type OpenAICompatibleKeyVault, @@ -10,11 +9,7 @@ import { import { clientApiKeyManager } from '@lobechat/utils/client'; import { ModelProvider } from 'model-bank'; -import { LOBE_CHAT_AUTH_HEADER, SECRET_XOR_KEY } from '@/envs/auth'; import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra'; -import { useUserStore } from '@/store/user'; -import { userProfileSelectors } from '@/store/user/selectors'; -import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation'; import { resolveRuntimeProvider } from './chat/helper'; @@ -104,15 +99,8 @@ export const getProviderAuthPayload = ( } }; -const createAuthTokenWithPayload = (payload = {}) => { - const userId = userProfileSelectors.userId(useUserStore.getState()); - - return obfuscatePayloadWithXOR<ClientSecretPayload>({ userId, ...payload }, SECRET_XOR_KEY); -}; - interface AuthParams { headers?: HeadersInit; - payload?: Record<string, any>; provider?: string; } @@ -128,19 +116,6 @@ export const createPayloadWithKeyVaults = (provider: string) => { }; }; -export const createXorKeyVaultsPayload = (provider: string) => { - const payload = createPayloadWithKeyVaults(provider); - return obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY); -}; - export const createHeaderWithAuth = async (params?: AuthParams): Promise<HeadersInit> => { - let payload = params?.payload || {}; - - if (params?.provider) { - payload = { ...payload, ...createPayloadWithKeyVaults(params?.provider) }; - } - - const token = createAuthTokenWithPayload(payload); - - return { ...params?.headers, [LOBE_CHAT_AUTH_HEADER]: token }; + return { ...params?.headers }; };
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
6- github.com/lobehub/lobehub/commit/3327b293d66c013f076cbc16cdbd05a61a3d0428nvdPatchWEB
- github.com/advisories/GHSA-5mwj-v5jw-5c97ghsaADVISORY
- github.com/lobehub/lobehub/security/advisories/GHSA-5mwj-v5jw-5c97nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-39411ghsaADVISORY
- github.com/lobehub/lobehub/pull/13535nvdIssue TrackingWEB
- github.com/lobehub/lobehub/releases/tag/v2.1.48nvdProductWEB
News mentions
0No linked articles in our index yet.