feat: add the market auth auto generate way (#10993)

* feat: add the market auth auto generate way

* feat: use market trusted client to have auto auth way

* chore: update deps
This commit is contained in:
Shinji-Li
2025-12-26 20:23:33 +08:00
committed by GitHub
parent 45996c6f23
commit 849ac733c7
20 changed files with 445 additions and 58 deletions
+1 -1
View File
@@ -201,7 +201,7 @@
"@lobehub/desktop-ipc-typings": "workspace:*",
"@lobehub/editor": "^3.1.1",
"@lobehub/icons": "^4.0.0",
"@lobehub/market-sdk": "^0.24.0",
"@lobehub/market-sdk": "^0.25.0",
"@lobehub/tts": "^4.0.0",
"@lobehub/ui": "^4.3.1",
"@modelcontextprotocol/sdk": "^1.25.1",
+1
View File
@@ -50,6 +50,7 @@ export interface GlobalServerConfig {
aiProvider: ServerLanguageModel;
defaultAgent?: PartialDeep<UserDefaultAgent>;
enableKlavis?: boolean;
enableMarketTrustedClient?: boolean;
enableUploadFileToServer?: boolean;
enabledAccessCode?: boolean;
/**
@@ -1,6 +1,8 @@
import { MarketSDK } from '@lobehub/market-sdk';
import { type NextRequest, NextResponse } from 'next/server';
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
type RouteContext = {
params: Promise<{
segments?: string[];
@@ -54,9 +56,12 @@ const notFound = (reason: string) =>
const handleAgent = async (req: NextRequest, segments: string[]) => {
const accessToken = extractAccessToken(req);
const trustedClientToken = await getTrustedClientTokenForSession();
const market = new MarketSDK({
accessToken,
baseURL: MARKET_BASE_URL,
trustedClientToken,
});
if (segments.length === 0) {
@@ -1,6 +1,8 @@
import { MarketSDK } from '@lobehub/market-sdk';
import { type NextRequest, NextResponse } from 'next/server';
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
type RouteContext = {
params: Promise<{
segments?: string[];
@@ -162,15 +164,38 @@ const handleProxy = async (req: NextRequest, context: RouteContext) => {
try {
const { token } = (await req.json()) as { token?: string };
// 如果没有 token,尝试使用 trustedClientToken
if (!token) {
return NextResponse.json(
{
error: 'missing_token',
message: 'Token is required for userinfo proxy.',
status: 'error',
const trustedClientToken = await getTrustedClientTokenForSession();
if (!trustedClientToken) {
return NextResponse.json(
{
error: 'missing_token',
message: 'Token is required for userinfo proxy.',
status: 'error',
},
{ status: 400 },
);
}
// 使用 trustedClientToken 直接调用 Market userinfo 端点
const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
const response = await fetch(userInfoUrl, {
headers: {
'Content-Type': 'application/json',
'x-lobe-trust-token': trustedClientToken,
},
{ status: 400 },
);
method: 'GET',
});
if (!response.ok) {
throw new Error(`Failed to fetch user info: ${response.status} ${response.statusText}`);
}
const userInfo = await response.json();
return NextResponse.json(userInfo);
}
const response = await market.auth.getUserInfo(token);
@@ -1,6 +1,8 @@
import { MarketSDK } from '@lobehub/market-sdk';
import { type NextRequest, NextResponse } from 'next/server';
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
type RouteContext = {
params: Promise<{
segments?: string[];
@@ -33,19 +35,22 @@ export const POST = async (req: NextRequest, context: RouteContext) => {
const { segments = [] } = await context.params;
const action = segments[0];
const accessToken = getAccessToken(req);
const trustedClientToken = await getTrustedClientTokenForSession();
if (!accessToken) {
const market = new MarketSDK({
accessToken,
baseURL: MARKET_BASE_URL,
trustedClientToken,
});
// Only require accessToken if trusted client token is not available
if (!accessToken && !trustedClientToken) {
return NextResponse.json(
{ error: 'unauthorized', message: 'Access token required' },
{ status: 401 },
);
}
const market = new MarketSDK({
accessToken,
baseURL: MARKET_BASE_URL,
});
try {
const body = await req.json();
@@ -131,10 +136,12 @@ export const GET = async (req: NextRequest, context: RouteContext) => {
const { segments = [] } = await context.params;
const action = segments[0];
const accessToken = getAccessToken(req);
const trustedClientToken = await getTrustedClientTokenForSession();
const market = new MarketSDK({
accessToken,
baseURL: MARKET_BASE_URL,
trustedClientToken,
});
const url = new URL(req.url);
@@ -1,6 +1,8 @@
import { MarketSDK } from '@lobehub/market-sdk';
import { type NextRequest, NextResponse } from 'next/server';
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
type RouteContext = {
params: Promise<{
username: string;
@@ -18,9 +20,11 @@ const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://mark
export const GET = async (req: NextRequest, context: RouteContext) => {
const { username } = await context.params;
const decodedUsername = decodeURIComponent(username);
const trustedClientToken = await getTrustedClientTokenForSession();
const market = new MarketSDK({
baseURL: MARKET_BASE_URL,
trustedClientToken,
});
try {
+19 -8
View File
@@ -1,6 +1,8 @@
import { MarketSDK } from '@lobehub/market-sdk';
import { type NextRequest, NextResponse } from 'next/server';
import { getTrustedClientTokenForSession } from '@/libs/trusted-client';
const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
const extractAccessToken = (req: NextRequest) => {
@@ -17,7 +19,7 @@ const extractAccessToken = (req: NextRequest) => {
* PUT /market/user/me
*
* Updates the authenticated user's profile information.
* Requires authentication via Bearer token.
* Requires authentication via Bearer token or trusted client token.
*
* Request body:
* - userName?: string - User's unique username
@@ -27,8 +29,16 @@ const extractAccessToken = (req: NextRequest) => {
*/
export const PUT = async (req: NextRequest) => {
const accessToken = extractAccessToken(req);
const trustedClientToken = await getTrustedClientTokenForSession();
if (!accessToken) {
const market = new MarketSDK({
accessToken,
baseURL: MARKET_BASE_URL,
trustedClientToken,
});
// Only require accessToken if trusted client token is not available
if (!accessToken && !trustedClientToken) {
return NextResponse.json(
{
error: 'unauthorized',
@@ -39,11 +49,6 @@ export const PUT = async (req: NextRequest) => {
);
}
const market = new MarketSDK({
accessToken,
baseURL: MARKET_BASE_URL,
});
try {
const payload = await req.json();
@@ -59,7 +64,13 @@ export const PUT = async (req: NextRequest) => {
);
}
const response = await market.user.updateUserInfo(payload);
// Ensure meta is at least an empty object
const normalizedPayload = {
...payload,
meta: payload.meta ?? {},
};
const response = await market.user.updateUserInfo(normalizedPayload);
return NextResponse.json(response);
} catch (error) {
@@ -7,6 +7,25 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useMarketAuth, useMarketUserProfile } from '@/layout/AuthProvider/MarketAuth';
import { useServerConfigStore } from '@/store/serverConfig';
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
/**
* 检查用户是否需要完善资料
* 当使用 trustedClient 自动授权时,用户的 meta 相关字段会为空
*/
const checkNeedsProfileSetup = (
enableMarketTrustedClient: boolean,
userProfile: { avatarUrl: string | null; bannerUrl: string | null; socialLinks: { github?: string; twitter?: string; website?: string } | null } | null | undefined,
): boolean => {
if (!enableMarketTrustedClient) return false;
if (!userProfile) return true;
// 如果 avatarUrl 字段为空,则需要完善资料
const hasAvatarUrl = !!userProfile.avatarUrl;
return !hasAvatarUrl;
};
const UserAvatar = memo(() => {
const { t } = useTranslation('discover');
@@ -14,15 +33,25 @@ const UserAvatar = memo(() => {
const [loading, setLoading] = useState(false);
const { isAuthenticated, isLoading, getCurrentUserInfo, signIn } = useMarketAuth();
const enableMarketTrustedClient = useServerConfigStore(serverConfigSelectors.enableMarketTrustedClient);
const userInfo = getCurrentUserInfo();
const username = userInfo?.sub;
// Use SWR to fetch user profile with caching
const { data: userProfile } = useMarketUserProfile(username);
// 检查是否需要完善资料
const needsProfileSetup = checkNeedsProfileSetup(enableMarketTrustedClient, userProfile);
console.log('needsProfileSetup', needsProfileSetup);
const handleSignIn = useCallback(async () => {
setLoading(true);
try {
// 统一调用 signIn,会先弹出确认弹窗
// trustedClient 模式下确认后会弹出 ProfileSetupModal
// OIDC 模式下确认后会走 OIDC 流程
await signIn();
} catch {
// User cancelled or error occurred
@@ -30,19 +59,19 @@ const UserAvatar = memo(() => {
setLoading(false);
}, [signIn]);
const handleNavigateToProfile = useCallback(() => {
// Use userName from profile for the URL (not OIDC sub/id)
const handleAvatarClick = useCallback(() => {
const profileUserName = userProfile?.userName || userProfile?.namespace;
if (profileUserName) {
navigate(`/community/user/${profileUserName}`);
}
}, [navigate, userProfile?.userName]);
}, [navigate, userProfile?.userName, userProfile?.namespace]);
if (isLoading) {
return <Skeleton.Avatar active shape={'square'} size={28} style={{ borderRadius: 6 }} />;
}
if (!isAuthenticated) {
// 未认证,或者是 trustedClient 模式但需要完善资料时,显示登录按钮
if (!isAuthenticated || needsProfileSetup) {
return (
<Button
icon={UserCircleIcon}
@@ -64,7 +93,7 @@ const UserAvatar = memo(() => {
return (
<Avatar
avatar={avatarUrl || userProfile?.userName}
onClick={handleNavigateToProfile}
onClick={handleAvatarClick}
shape={'square'}
size={28}
/>
+17
View File
@@ -61,6 +61,20 @@ export const getAppConfig = () => {
SSRF_ALLOW_IP_ADDRESS_LIST: z.string().optional(),
MARKET_BASE_URL: z.string().optional(),
/**
* Trusted Client Secret for Market API authentication
* 64-character hex string (32 bytes) shared with Market server
* Used to encrypt user payload for trusted client authentication
* Generate with: openssl rand -hex 32
*/
MARKET_TRUSTED_CLIENT_SECRET: z.string().length(83).optional(),
/**
* Trusted Client ID for Market API authentication
* Must be registered in Market's TRUSTED_CLIENT_IDS whitelist
* e.g., "lobechat-com", "lobehub-desktop"
*/
MARKET_TRUSTED_CLIENT_ID: z.string().optional(),
/**
* Enable Queue-based Agent Runtime
* When true, use QStash for async agent execution (production)
@@ -103,6 +117,9 @@ export const getAppConfig = () => {
SSRF_ALLOW_IP_ADDRESS_LIST: process.env.SSRF_ALLOW_IP_ADDRESS_LIST,
MARKET_BASE_URL: process.env.MARKET_BASE_URL,
MARKET_TRUSTED_CLIENT_SECRET: process.env.MARKET_TRUSTED_CLIENT_SECRET,
MARKET_TRUSTED_CLIENT_ID: process.env.MARKET_TRUSTED_CLIENT_ID,
enableQueueAgentRuntime: process.env.AGENT_RUNTIME_MODE === 'queue',
},
});
@@ -6,6 +6,8 @@ import { useTranslation } from 'react-i18next';
import { mutate as globalMutate } from 'swr';
import { MARKET_ENDPOINTS, MARKET_OIDC_ENDPOINTS } from '@/services/_url';
import { useServerConfigStore } from '@/store/serverConfig';
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
import { useUserStore } from '@/store/user';
import { settingsSelectors } from '@/store/user/slices/settings/selectors/settings';
@@ -31,8 +33,9 @@ interface MarketAuthProviderProps {
/**
* 获取用户信息(从 OIDC userinfo endpoint
* @param accessToken - 可选的 access token,如果不传则后端会尝试使用 trustedClientToken
*/
const fetchUserInfo = async (accessToken: string): Promise<MarketUserInfo | null> => {
const fetchUserInfo = async (accessToken?: string): Promise<MarketUserInfo | null> => {
try {
const response = await fetch(MARKET_OIDC_ENDPOINTS.userinfo, {
body: JSON.stringify({ token: accessToken }),
@@ -173,6 +176,9 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
// 订阅 user store 的初始化状态,当 isUserStateInit 为 true 时,settings 数据已加载完成
const isUserStateInit = useUserStore((s) => s.isUserStateInit);
// 检查是否启用了 Market Trusted Client 认证
const enableMarketTrustedClient = useServerConfigStore(serverConfigSelectors.enableMarketTrustedClient);
// 初始化 OIDC 客户端(仅在客户端)
useEffect(() => {
if (typeof window !== 'undefined') {
@@ -200,6 +206,32 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
const initializeSession = async () => {
setStatus('loading');
// 如果启用了 Trusted Client 认证,直接通过后端获取用户信息(不传 token)
if (enableMarketTrustedClient) {
const userInfo = await fetchUserInfo();
if (userInfo) {
// 使用 Trusted Client 时,创建一个虚拟的 session(无需真实 token
const trustedSession: MarketAuthSession = {
accessToken: '', // Trusted Client 不需要前端 token
expiresAt: Number.MAX_SAFE_INTEGER, // 不过期
expiresIn: Number.MAX_SAFE_INTEGER,
scope: 'openid profile email',
tokenType: 'Bearer',
userInfo,
};
setSession(trustedSession);
setStatus('authenticated');
return;
}
// 如果获取失败,设置为未认证状态
setStatus('unauthenticated');
return;
}
// 原有的 OIDC token 认证流程
const dbTokens = getMarketTokensFromDB();
// 检查 DB 中是否有 token
@@ -324,6 +356,20 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
*/
const handleConfirmAuth = async () => {
setShowConfirmModal(false);
// 如果是 trustedClient 模式,直接打开 ProfileSetupModal 完善资料
if (enableMarketTrustedClient) {
setIsFirstTimeSetup(true);
setShowProfileSetupModal(true);
if (pendingSignInResolve) {
pendingSignInResolve(session?.userInfo?.accountId ?? null);
setPendingSignInResolve(null);
setPendingSignInReject(null);
}
return;
}
// 原有的 OIDC 流程
try {
const result = await handleActualSignIn();
if (pendingSignInResolve) {
@@ -415,13 +461,14 @@ export const MarketAuthProvider = ({ children, isDesktop }: MarketAuthProviderPr
if (isUserStateInit) {
initializeSession();
}
}, [isUserStateInit]);
}, [isUserStateInit, enableMarketTrustedClient]);
const contextValue: MarketAuthContextType = {
getAccessToken,
getCurrentUserInfo,
getRefreshToken,
isAuthenticated: status === 'authenticated',
// 当启用 Trusted Client 认证时,自动视为已认证(后端会自动使用 trustedClientToken
isAuthenticated: enableMarketTrustedClient || status === 'authenticated',
isLoading: status === 'loading',
openProfileSetup,
refreshToken,
@@ -13,6 +13,10 @@ import { MARKET_ENDPOINTS } from '@/services/_url';
import { useFileStore } from '@/store/file';
import { useGlobalStore } from '@/store/global';
import { globalGeneralSelectors } from '@/store/global/selectors';
import { useServerConfigStore } from '@/store/serverConfig';
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
import { useUserStore } from '@/store/user';
import { userProfileSelectors } from '@/store/user/selectors';
import { type MarketUserProfile } from './types';
@@ -66,6 +70,12 @@ const ProfileSetupModal = memo<ProfileSetupModalProps>(
const [loading, setLoading] = useState(false);
const locale = useGlobalStore(globalGeneralSelectors.currentLanguage);
// 检查是否是自动授权模式
const enableMarketTrustedClient = useServerConfigStore(serverConfigSelectors.enableMarketTrustedClient);
// 获取当前用户头像作为默认值
const currentUserAvatar = useUserStore(userProfileSelectors.userAvatar);
// Avatar state
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [avatarUploading, setAvatarUploading] = useState(false);
@@ -99,10 +109,11 @@ const ProfileSetupModal = memo<ProfileSetupModalProps>(
});
// Reset avatar and banner
setAvatarUrl(userProfile?.avatarUrl || null);
// 如果 userProfile 有 avatarUrl 就用它,否则用当前用户头像作为默认值
setAvatarUrl(userProfile?.avatarUrl || currentUserAvatar || null);
setBannerUrl(userProfile?.bannerUrl || null);
}
}, [open, userProfile, defaultDisplayName, form]);
}, [open, userProfile, defaultDisplayName, form, currentUserAvatar]);
// Handle avatar change (emoji)
const handleAvatarChange = useCallback((emoji: string) => {
@@ -173,7 +184,8 @@ const ProfileSetupModal = memo<ProfileSetupModalProps>(
}, []);
const handleSubmit = useCallback(async () => {
if (!accessToken) {
// 如果不是自动授权模式,需要校验 accessToken
if (!enableMarketTrustedClient && !accessToken) {
message.error(t('profileSetup.errors.notAuthenticated'));
return;
}
@@ -233,7 +245,7 @@ const ProfileSetupModal = memo<ProfileSetupModalProps>(
} finally {
setLoading(false);
}
}, [accessToken, avatarUrl, bannerUrl, form, message, onClose, onSuccess, t]);
}, [accessToken, avatarUrl, bannerUrl, enableMarketTrustedClient, form, message, onClose, onSuccess, t]);
const handleCancel = useCallback(() => {
if (!isFirstTimeSetup) {
+1
View File
@@ -1,2 +1,3 @@
export * from './keyVaults';
export * from './marketUserInfo';
export * from './serverDatabase';
@@ -0,0 +1,51 @@
import type { LobeChatDatabase } from '@lobechat/database';
import { UserModel } from '@/database/models/user';
import type { TrustedClientUserInfo } from '@/libs/trusted-client';
import { trpc } from '../init';
interface ContextWithServerDB {
serverDB?: LobeChatDatabase;
userId?: string | null;
}
/**
* Middleware that fetches user info for Market trusted client authentication
* This requires serverDatabase middleware to be applied first
*/
export const marketUserInfo = trpc.middleware(async (opts) => {
const ctx = opts.ctx as ContextWithServerDB;
// If userId or serverDB is not available, skip fetching user info
if (!ctx.userId || !ctx.serverDB) {
return opts.next({
ctx: { marketUserInfo: undefined },
});
}
try {
const user = await UserModel.findById(ctx.serverDB, ctx.userId);
if (!user || !user.email) {
return opts.next({
ctx: { marketUserInfo: undefined },
});
}
const marketUserInfo: TrustedClientUserInfo = {
email: user.email,
name: user.fullName || user.username || undefined,
userId: ctx.userId,
};
return opts.next({
ctx: { marketUserInfo },
});
} catch {
// If fetching user info fails, continue without it
return opts.next({
ctx: { marketUserInfo: undefined },
});
}
});
+66
View File
@@ -0,0 +1,66 @@
import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth';
import type { TrustedClientUserInfo } from './index';
/**
* Get user info from the current session for trusted client authentication
* This works with different authentication providers (Clerk, BetterAuth, NextAuth)
*
* @returns User info or undefined if not authenticated
*/
export const getSessionUser = async (): Promise<TrustedClientUserInfo | undefined> => {
try {
if (enableClerk) {
const { currentUser } = await import('@clerk/nextjs/server');
const user = await currentUser();
if (!user?.id || !user?.primaryEmailAddress?.emailAddress) {
return undefined;
}
return {
email: user.primaryEmailAddress.emailAddress,
name: user.fullName || user.firstName || undefined,
userId: user.id,
};
}
if (enableBetterAuth) {
const { headers } = await import('next/headers');
const { auth } = await import('@/auth');
const headersList = await headers();
const session = await auth.api.getSession({
headers: headersList,
});
if (!session?.user?.id || !session?.user?.email) {
return undefined;
}
return {
email: session.user.email,
name: session.user.name || undefined,
userId: session.user.id,
};
}
if (enableNextAuth) {
const { default: NextAuth } = await import('@/libs/next-auth');
const session = await NextAuth.auth();
if (!session?.user?.id || !session?.user?.email) {
return undefined;
}
return {
email: session.user.email,
name: session.user.name || undefined,
userId: session.user.id,
};
}
return undefined;
} catch {
return undefined;
}
};
+68
View File
@@ -0,0 +1,68 @@
import { buildTrustedClientPayload, createTrustedClientToken } from '@lobehub/market-sdk';
import { appEnv } from '@/envs/app';
export interface TrustedClientUserInfo {
email: string;
name?: string;
userId: string;
}
export { getSessionUser } from './getSessionUser';
/**
* Check if trusted client authentication is enabled
*/
export const isTrustedClientEnabled = (): boolean => {
return !!(appEnv.MARKET_TRUSTED_CLIENT_SECRET && appEnv.MARKET_TRUSTED_CLIENT_ID);
};
/**
* Generate trusted client token for a specific user
* This token includes encrypted user info and is used for Market API authentication
*
* @param userInfo - User information (userId, email, name)
* @returns Encrypted token string or undefined if not configured
*/
export const generateTrustedClientToken = (userInfo: TrustedClientUserInfo): string | undefined => {
const { MARKET_TRUSTED_CLIENT_SECRET, MARKET_TRUSTED_CLIENT_ID } = appEnv;
if (!MARKET_TRUSTED_CLIENT_SECRET || !MARKET_TRUSTED_CLIENT_ID) {
return undefined;
}
if (!userInfo.email) {
return undefined;
}
try {
const payload = buildTrustedClientPayload({
clientId: MARKET_TRUSTED_CLIENT_ID,
email: userInfo.email,
name: userInfo.name,
userId: userInfo.userId,
});
return createTrustedClientToken(payload, MARKET_TRUSTED_CLIENT_SECRET);
} catch (error) {
console.error('Failed to generate trusted client token:', error);
return undefined;
}
};
/**
* Get trusted client token for the current session user
* This is a convenience function that combines getSessionUser and generateTrustedClientToken
*
* @returns Encrypted token string or undefined if not configured or user not authenticated
*/
export const getTrustedClientTokenForSession = async (): Promise<string | undefined> => {
const { getSessionUser } = await import('./getSessionUser');
const userInfo = await getSessionUser();
if (!userInfo) {
return undefined;
}
return generateTrustedClientToken(userInfo);
};
+1
View File
@@ -66,6 +66,7 @@ export const getServerGlobalConfig = async () => {
config: parseAgentConfig(DEFAULT_AGENT_CONFIG),
},
enableKlavis: !!klavisEnv.KLAVIS_API_KEY,
enableMarketTrustedClient: !!(appEnv.MARKET_TRUSTED_CLIENT_SECRET && appEnv.MARKET_TRUSTED_CLIENT_ID),
enableUploadFileToServer: !!fileEnv.S3_SECRET_ACCESS_KEY,
enabledAccessCode: ACCESS_CODES?.length > 0,
+30 -17
View File
@@ -6,7 +6,7 @@ import { z } from 'zod';
import { type ToolCallContent } from '@/libs/mcp';
import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
import { DiscoverService } from '@/server/services/discover';
import { FileService } from '@/server/services/file';
import {
@@ -26,27 +26,40 @@ const log = debug('lambda-router:market');
const marketSourceSchema = z.enum(['legacy', 'new']);
const marketProcedure = publicProcedure.use(async ({ ctx, next }) => {
return next({
ctx: {
discoverService: new DiscoverService({ accessToken: ctx.marketAccessToken }),
},
// Public procedure with optional user info for trusted client token
const marketProcedure = publicProcedure
.use(serverDatabase)
.use(marketUserInfo)
.use(async ({ ctx, next }) => {
return next({
ctx: {
discoverService: new DiscoverService({
accessToken: ctx.marketAccessToken,
userInfo: ctx.marketUserInfo,
}),
},
});
});
});
// Procedure with user authentication for operations requiring user access token
const authedMarketProcedure = authedProcedure.use(serverDatabase).use(async ({ ctx, next }) => {
const { UserModel } = await import('@/database/models/user');
const userModel = new UserModel(ctx.serverDB, ctx.userId);
const authedMarketProcedure = authedProcedure
.use(serverDatabase)
.use(marketUserInfo)
.use(async ({ ctx, next }) => {
const { UserModel } = await import('@/database/models/user');
const userModel = new UserModel(ctx.serverDB, ctx.userId);
return next({
ctx: {
discoverService: new DiscoverService({ accessToken: ctx.marketAccessToken }),
fileService: new FileService(ctx.serverDB, ctx.userId),
userModel,
},
return next({
ctx: {
discoverService: new DiscoverService({
accessToken: ctx.marketAccessToken,
userInfo: ctx.marketUserInfo,
}),
fileService: new FileService(ctx.serverDB, ctx.userId),
userModel,
},
});
});
});
export const marketRouter = router({
// ============================== Cloud MCP Gateway ==============================
+11 -4
View File
@@ -5,12 +5,13 @@ import { z } from 'zod';
import { DocumentModel } from '@/database/models/document';
import { FileModel } from '@/database/models/file';
import { authedProcedure, router } from '@/libs/trpc/lambda';
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
import { generateTrustedClientToken } from '@/libs/trusted-client';
import { FileS3 } from '@/server/modules/S3';
const log = debug('lobe-server:tools:code-interpreter');
const codeInterpreterProcedure = authedProcedure;
const codeInterpreterProcedure = authedProcedure.use(serverDatabase).use(marketUserInfo);
// Schema for tool call request
const callToolSchema = z.object({
@@ -82,7 +83,7 @@ export interface SaveExportedFileContentResult {
}
export const codeInterpreterRouter = router({
callTool: codeInterpreterProcedure.input(callToolSchema).mutation(async ({ input }) => {
callTool: codeInterpreterProcedure.input(callToolSchema).mutation(async ({ input, ctx }) => {
const { toolName, params, userId, topicId, marketAccessToken } = input;
log('Calling cloud code interpreter tool: %s with params: %O', toolName, {
@@ -92,11 +93,17 @@ export const codeInterpreterRouter = router({
});
log('Market access token available: %s', marketAccessToken ? 'yes' : 'no');
// Generate trusted client token if user info is available
const trustedClientToken = ctx.marketUserInfo
? generateTrustedClientToken(ctx.marketUserInfo)
: undefined;
try {
// Initialize MarketSDK with market access token from OIDC (passed via input)
// Initialize MarketSDK with market access token and trusted client token
const market = new MarketSDK({
accessToken: marketAccessToken,
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
trustedClientToken,
});
// Call market-sdk's runBuildInTool
+24 -3
View File
@@ -42,7 +42,12 @@ import {
getTextInputUnitRate,
getTextOutputUnitRate,
} from '@lobechat/utils';
import { type CategoryItem, type CategoryListQuery, MarketSDK, type UserInfoResponse } from '@lobehub/market-sdk';
import {
type CategoryItem,
type CategoryListQuery,
MarketSDK,
type UserInfoResponse,
} from '@lobehub/market-sdk';
import { type CallReportRequest, type InstallReportRequest } from '@lobehub/market-types';
import dayjs from 'dayjs';
import debug from 'debug';
@@ -50,25 +55,41 @@ import { cloneDeep, countBy, isString, merge, uniq, uniqBy } from 'es-toolkit/co
import matter from 'gray-matter';
import urlJoin from 'url-join';
import { type TrustedClientUserInfo, generateTrustedClientToken } from '@/libs/trusted-client';
import { normalizeLocale } from '@/locales/resources';
import { AssistantStore } from '@/server/modules/AssistantStore';
import { PluginStore } from '@/server/modules/PluginStore';
const log = debug('lobe-server:discover');
export interface DiscoverServiceOptions {
/** Access token from OIDC flow (legacy) */
accessToken?: string;
/** User info for generating trusted client token */
userInfo?: TrustedClientUserInfo;
}
export class DiscoverService {
assistantStore = new AssistantStore();
pluginStore = new PluginStore();
market: MarketSDK;
constructor({ accessToken }: { accessToken?: string } = {}) {
constructor(options: DiscoverServiceOptions = {}) {
const { accessToken, userInfo } = options;
// Generate trusted client token if user info is available
const trustedClientToken = userInfo ? generateTrustedClientToken(userInfo) : undefined;
this.market = new MarketSDK({
accessToken,
baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
trustedClientToken,
});
log(
'DiscoverService initialized with market baseURL: %s',
'DiscoverService initialized with market baseURL: %s, hasTrustedToken: %s, userId: %s',
process.env.NEXT_PUBLIC_MARKET_BASE_URL,
!!trustedClientToken,
userInfo?.userId,
);
}
+1
View File
@@ -4,6 +4,7 @@ export const featureFlagsSelectors = (s: ServerConfigStore) => s.featureFlags;
export const serverConfigSelectors = {
enableKlavis: (s: ServerConfigStore) => s.serverConfig.enableKlavis || false,
enableMarketTrustedClient: (s: ServerConfigStore) => s.serverConfig.enableMarketTrustedClient || false,
enableUploadFileToServer: (s: ServerConfigStore) => s.serverConfig.enableUploadFileToServer,
enabledAccessCode: (s: ServerConfigStore) => !!s.serverConfig?.enabledAccessCode,
enabledTelemetryChat: (s: ServerConfigStore) => s.serverConfig.telemetry.langfuse || false,