@lobehub/lobehub 2.0.0-next.224 → 2.0.0-next.226

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.github/workflows/test.yml +18 -14
  2. package/CHANGELOG.md +50 -0
  3. package/changelog/v1.json +14 -0
  4. package/locales/en-US/common.json +3 -2
  5. package/locales/en-US/setting.json +4 -0
  6. package/locales/zh-CN/setting.json +4 -0
  7. package/package.json +2 -2
  8. package/packages/database/src/models/user.ts +33 -0
  9. package/packages/database/src/repositories/knowledge/index.ts +1 -1
  10. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/ForkConfirmModal.tsx +67 -0
  11. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/PublishButton.tsx +92 -105
  12. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/index.tsx +13 -48
  13. package/src/app/[variants]/(main)/chat/profile/features/Header/AgentPublishButton/useMarketPublish.ts +69 -93
  14. package/src/business/client/hooks/useRenderBusinessChatErrorMessageExtra.tsx +10 -0
  15. package/src/features/CommandMenu/ContextCommands.tsx +97 -37
  16. package/src/features/CommandMenu/SearchResults.tsx +100 -276
  17. package/src/features/CommandMenu/components/CommandItem.tsx +1 -1
  18. package/src/features/CommandMenu/utils/contextCommands.ts +56 -1
  19. package/src/features/Conversation/Error/index.tsx +7 -1
  20. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +11 -28
  21. package/src/layout/AuthProvider/MarketAuth/ProfileSetupModal.tsx +30 -25
  22. package/src/layout/AuthProvider/MarketAuth/useMarketUserProfile.ts +4 -9
  23. package/src/libs/redis/manager.ts +51 -15
  24. package/src/libs/trpc/lambda/middleware/index.ts +1 -0
  25. package/src/libs/trpc/lambda/middleware/marketSDK.ts +68 -0
  26. package/src/locales/default/common.ts +2 -2
  27. package/src/locales/default/setting.ts +5 -0
  28. package/src/server/routers/lambda/market/agent.ts +504 -0
  29. package/src/server/routers/lambda/market/index.ts +17 -0
  30. package/src/server/routers/lambda/market/oidc.ts +169 -0
  31. package/src/server/routers/lambda/market/social.ts +532 -0
  32. package/src/server/routers/lambda/market/user.ts +123 -0
  33. package/src/services/marketApi.ts +24 -84
  34. package/src/services/social.ts +70 -166
@@ -0,0 +1,504 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import debug from 'debug';
3
+ import { customAlphabet } from 'nanoid/non-secure';
4
+ import { z } from 'zod';
5
+
6
+ import { authedProcedure, router } from '@/libs/trpc/lambda';
7
+ import { marketSDK, marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
8
+ import { type TrustedClientUserInfo, generateTrustedClientToken } from '@/libs/trusted-client';
9
+
10
+ const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
11
+
12
+ interface MarketUserInfo {
13
+ accountId: number;
14
+ sub: string;
15
+ }
16
+
17
+ const log = debug('lambda-router:market:agent');
18
+
19
+ /**
20
+ * Generate a market identifier (8-character lowercase alphanumeric string)
21
+ * Format: [a-z0-9]{8}
22
+ */
23
+ const generateMarketIdentifier = () => {
24
+ const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
25
+ const generate = customAlphabet(alphabet, 8);
26
+ return generate();
27
+ };
28
+
29
+ interface FetchMarketUserInfoOptions {
30
+ accessToken?: string;
31
+ userInfo?: TrustedClientUserInfo;
32
+ }
33
+
34
+ /**
35
+ * Fetch Market user info using either trustedClientToken or accessToken
36
+ * Returns the Market accountId which is different from LobeChat userId
37
+ *
38
+ * Priority:
39
+ * 1. trustedClientToken (if userInfo is provided and TRUSTED_CLIENT_SECRET is configured)
40
+ * 2. accessToken (if provided, from OIDC flow)
41
+ */
42
+ const fetchMarketUserInfo = async (
43
+ options: FetchMarketUserInfoOptions,
44
+ ): Promise<MarketUserInfo | null> => {
45
+ const { userInfo, accessToken } = options;
46
+
47
+ try {
48
+ const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
49
+ const headers: Record<string, string> = {
50
+ 'Content-Type': 'application/json',
51
+ };
52
+
53
+ // Try trustedClientToken first (if userInfo is available)
54
+ if (userInfo) {
55
+ const trustedClientToken = generateTrustedClientToken(userInfo);
56
+ if (trustedClientToken) {
57
+ headers['x-lobe-trust-token'] = trustedClientToken;
58
+ log('Using trustedClientToken for user info fetch');
59
+ }
60
+ }
61
+
62
+ // Fall back to accessToken if no trustedClientToken
63
+ if (!headers['x-lobe-trust-token'] && accessToken) {
64
+ headers['Authorization'] = `Bearer ${accessToken}`;
65
+ log('Using accessToken for user info fetch');
66
+ }
67
+
68
+ // If neither authentication method is available, return null
69
+ if (!headers['x-lobe-trust-token'] && !headers['Authorization']) {
70
+ log('No authentication method available for fetching user info');
71
+ return null;
72
+ }
73
+
74
+ const response = await fetch(userInfoUrl, {
75
+ headers,
76
+ method: 'GET',
77
+ });
78
+
79
+ if (!response.ok) {
80
+ log('Failed to fetch Market user info: %s %s', response.status, response.statusText);
81
+ return null;
82
+ }
83
+
84
+ return (await response.json()) as MarketUserInfo;
85
+ } catch (error) {
86
+ log('Error fetching Market user info: %O', error);
87
+ return null;
88
+ }
89
+ };
90
+
91
+ // Authenticated procedure for agent management
92
+ // Requires user to be logged in and has MarketSDK initialized
93
+ // Also fetches user's market accessToken from database for OIDC authentication
94
+ const agentProcedure = authedProcedure
95
+ .use(serverDatabase)
96
+ .use(marketUserInfo)
97
+ .use(marketSDK)
98
+ .use(async ({ ctx, next }) => {
99
+ // Import UserModel dynamically to avoid circular dependencies
100
+ const { UserModel } = await import('@/database/models/user');
101
+ const userModel = new UserModel(ctx.serverDB, ctx.userId);
102
+
103
+ // Get user's market accessToken from database (stored by MarketAuthProvider after OIDC login)
104
+ let marketOidcAccessToken: string | undefined;
105
+ try {
106
+ const userState = await userModel.getUserState(async () => ({}));
107
+ marketOidcAccessToken = userState.settings?.market?.accessToken;
108
+ log('marketOidcAccessToken from DB exists=%s', !!marketOidcAccessToken);
109
+ } catch (error) {
110
+ log('Failed to get marketOidcAccessToken from DB: %O', error);
111
+ }
112
+
113
+ return next({
114
+ ctx: {
115
+ marketOidcAccessToken,
116
+ },
117
+ });
118
+ });
119
+
120
+ // Schema definitions
121
+ const createAgentSchema = z.object({
122
+ homepage: z.string().optional(),
123
+ identifier: z.string(),
124
+ isFeatured: z.boolean().optional(),
125
+ name: z.string(),
126
+ status: z.enum(['published', 'unpublished', 'archived', 'deprecated']).optional(),
127
+ tokenUsage: z.number().optional(),
128
+ visibility: z.enum(['public', 'private', 'internal']).optional(),
129
+ });
130
+
131
+ const createAgentVersionSchema = z.object({
132
+ a2aProtocolVersion: z.string().optional(),
133
+ avatar: z.string().optional(),
134
+ category: z.string().optional(),
135
+ changelog: z.string().optional(),
136
+ config: z.record(z.any()).optional(),
137
+ defaultInputModes: z.array(z.string()).optional(),
138
+ defaultOutputModes: z.array(z.string()).optional(),
139
+ description: z.string().optional(),
140
+ documentationUrl: z.string().optional(),
141
+ extensions: z.array(z.record(z.any())).optional(),
142
+ hasPushNotifications: z.boolean().optional(),
143
+ hasStateTransitionHistory: z.boolean().optional(),
144
+ hasStreaming: z.boolean().optional(),
145
+ identifier: z.string(),
146
+ interfaces: z.array(z.record(z.any())).optional(),
147
+ name: z.string().optional(),
148
+ preferredTransport: z.string().optional(),
149
+ providerId: z.number().optional(),
150
+ securityRequirements: z.array(z.record(z.any())).optional(),
151
+ securitySchemes: z.record(z.any()).optional(),
152
+ setAsCurrent: z.boolean().optional(),
153
+ summary: z.string().optional(),
154
+ supportsAuthenticatedExtendedCard: z.boolean().optional(),
155
+ tokenUsage: z.number().optional(),
156
+ url: z.string().optional(),
157
+ });
158
+
159
+ const paginationSchema = z.object({
160
+ page: z.number().optional(),
161
+ pageSize: z.number().optional(),
162
+ });
163
+
164
+ // Schema for the unified publish/create flow
165
+ const publishOrCreateSchema = z.object({
166
+
167
+ // Version data
168
+ avatar: z.string().optional(),
169
+
170
+
171
+ category: z.string().optional(),
172
+
173
+
174
+
175
+
176
+ changelog: z.string().optional(),
177
+
178
+
179
+ config: z.record(z.any()).optional(),
180
+
181
+
182
+ description: z.string().optional(),
183
+
184
+
185
+ editorData: z.record(z.any()).optional(),
186
+
187
+ // Agent basic info
188
+ identifier: z.string().optional(),
189
+ // Optional - if not provided or not owned, will create new
190
+ name: z.string(),
191
+ tags: z.array(z.string()).optional(),
192
+ tokenUsage: z.number().optional(),
193
+ });
194
+
195
+ export const agentRouter = router({
196
+ /**
197
+ * Check if current user owns the specified agent
198
+ * Returns ownership status and original agent info for fork scenario
199
+ */
200
+ checkOwnership: agentProcedure
201
+ .input(z.object({ identifier: z.string() }))
202
+ .query(async ({ input, ctx }) => {
203
+ log('checkOwnership input: %O', input);
204
+
205
+ try {
206
+ // Get agent detail
207
+ const agentDetail = await ctx.marketSDK.agents.getAgentDetail(input.identifier);
208
+
209
+ if (!agentDetail) {
210
+ return {
211
+ exists: false,
212
+ isOwner: false,
213
+ originalAgent: null,
214
+ };
215
+ }
216
+
217
+ // Get Market user info to get accountId
218
+ // Support both trustedClientToken and OIDC accessToken authentication
219
+ const userInfo = ctx.marketUserInfo as TrustedClientUserInfo | undefined;
220
+ const accessToken = (ctx as { marketOidcAccessToken?: string }).marketOidcAccessToken;
221
+ let currentAccountId: number | null = null;
222
+
223
+ const marketUserInfoResult = await fetchMarketUserInfo({ accessToken, userInfo });
224
+ currentAccountId = marketUserInfoResult?.accountId ?? null;
225
+
226
+ const ownerId = agentDetail.ownerId;
227
+ const isOwner = currentAccountId !== null && `${ownerId}` === `${currentAccountId}`;
228
+
229
+ log(
230
+ 'checkOwnership result: isOwner=%s, currentAccountId=%s, ownerId=%s',
231
+ isOwner,
232
+ currentAccountId,
233
+ ownerId,
234
+ );
235
+
236
+ return {
237
+ exists: true,
238
+ isOwner,
239
+ originalAgent: isOwner
240
+ ? null
241
+ : {
242
+ author: agentDetail.author,
243
+ avatar: agentDetail.avatar,
244
+ identifier: agentDetail.identifier,
245
+ name: agentDetail.name,
246
+ },
247
+ };
248
+ } catch (error) {
249
+ log('Error checking ownership: %O', error);
250
+ // If agent not found or error, treat as not existing
251
+ return {
252
+ exists: false,
253
+ isOwner: false,
254
+ originalAgent: null,
255
+ };
256
+ }
257
+ }),
258
+
259
+ /**
260
+ * Create a new agent in the marketplace
261
+ * POST /market/agent/create
262
+ */
263
+ createAgent: agentProcedure.input(createAgentSchema).mutation(async ({ input, ctx }) => {
264
+ log('createAgent input: %O', input);
265
+
266
+ try {
267
+ const response = await ctx.marketSDK.agents.createAgent(input);
268
+ return response;
269
+ } catch (error) {
270
+ log('Error creating agent: %O', error);
271
+ throw new TRPCError({
272
+ cause: error,
273
+ code: 'INTERNAL_SERVER_ERROR',
274
+ message: error instanceof Error ? error.message : 'Failed to create agent',
275
+ });
276
+ }
277
+ }),
278
+
279
+ /**
280
+ * Create a new version for an existing agent
281
+ * POST /market/agent/versions/create
282
+ */
283
+ createAgentVersion: agentProcedure
284
+ .input(createAgentVersionSchema)
285
+ .mutation(async ({ input, ctx }) => {
286
+ log('createAgentVersion input: %O', input);
287
+
288
+ try {
289
+ const response = await ctx.marketSDK.agents.createAgentVersion(input);
290
+ return response;
291
+ } catch (error) {
292
+ log('Error creating agent version: %O', error);
293
+ throw new TRPCError({
294
+ cause: error,
295
+ code: 'INTERNAL_SERVER_ERROR',
296
+ message: error instanceof Error ? error.message : 'Failed to create agent version',
297
+ });
298
+ }
299
+ }),
300
+
301
+ /**
302
+ * Deprecate an agent (permanently hide, cannot be republished)
303
+ * POST /market/agent/:identifier/deprecate
304
+ */
305
+ deprecateAgent: agentProcedure
306
+ .input(z.object({ identifier: z.string() }))
307
+ .mutation(async ({ input, ctx }) => {
308
+ log('deprecateAgent input: %O', input);
309
+
310
+ try {
311
+ const response = await ctx.marketSDK.agents.deprecate(input.identifier);
312
+ return response ?? { success: true };
313
+ } catch (error) {
314
+ log('Error deprecating agent: %O', error);
315
+ throw new TRPCError({
316
+ cause: error,
317
+ code: 'INTERNAL_SERVER_ERROR',
318
+ message: error instanceof Error ? error.message : 'Failed to deprecate agent',
319
+ });
320
+ }
321
+ }),
322
+
323
+ /**
324
+ * Get agent detail by identifier
325
+ * GET /market/agent/:identifier
326
+ */
327
+ getAgentDetail: agentProcedure
328
+ .input(z.object({ identifier: z.string() }))
329
+ .query(async ({ input, ctx }) => {
330
+ log('getAgentDetail input: %O', input);
331
+
332
+ try {
333
+ const response = await ctx.marketSDK.agents.getAgentDetail(input.identifier);
334
+ return response;
335
+ } catch (error) {
336
+ log('Error getting agent detail: %O', error);
337
+ throw new TRPCError({
338
+ cause: error,
339
+ code: 'INTERNAL_SERVER_ERROR',
340
+ message: error instanceof Error ? error.message : 'Failed to get agent detail',
341
+ });
342
+ }
343
+ }),
344
+
345
+ /**
346
+ * Get own agents (requires authentication)
347
+ * GET /market/agent/own
348
+ */
349
+ getOwnAgents: agentProcedure.input(paginationSchema.optional()).query(async ({ input, ctx }) => {
350
+ log('getOwnAgents input: %O', input);
351
+
352
+ try {
353
+ const response = await ctx.marketSDK.agents.getOwnAgents({
354
+ page: input?.page,
355
+ pageSize: input?.pageSize,
356
+ });
357
+ return response;
358
+ } catch (error) {
359
+ log('Error getting own agents: %O', error);
360
+ throw new TRPCError({
361
+ cause: error,
362
+ code: 'INTERNAL_SERVER_ERROR',
363
+ message: error instanceof Error ? error.message : 'Failed to get own agents',
364
+ });
365
+ }
366
+ }),
367
+
368
+ /**
369
+ * Publish an agent (make it visible in marketplace)
370
+ * POST /market/agent/:identifier/publish
371
+ */
372
+ publishAgent: agentProcedure
373
+ .input(z.object({ identifier: z.string() }))
374
+ .mutation(async ({ input, ctx }) => {
375
+ log('publishAgent input: %O', input);
376
+
377
+ try {
378
+ const response = await ctx.marketSDK.agents.publish(input.identifier);
379
+ return response ?? { success: true };
380
+ } catch (error) {
381
+ log('Error publishing agent: %O', error);
382
+ throw new TRPCError({
383
+ cause: error,
384
+ code: 'INTERNAL_SERVER_ERROR',
385
+ message: error instanceof Error ? error.message : 'Failed to publish agent',
386
+ });
387
+ }
388
+ }),
389
+
390
+ /**
391
+ * Unified publish or create agent flow
392
+ * This procedure handles the complete publish logic:
393
+ * 1. Check if identifier exists and if current user is owner
394
+ * 2. If not owner or no identifier, create new agent
395
+ * 3. Create new version for the agent
396
+ *
397
+ * Returns: { identifier, isNewAgent, success }
398
+ */
399
+ publishOrCreate: agentProcedure.input(publishOrCreateSchema).mutation(async ({ input, ctx }) => {
400
+ log('publishOrCreate input: %O', input);
401
+
402
+ const { identifier: inputIdentifier, name, ...versionData } = input;
403
+ let finalIdentifier = inputIdentifier;
404
+ let isNewAgent = false;
405
+
406
+ try {
407
+ // Step 1: Check ownership if identifier is provided
408
+ if (inputIdentifier) {
409
+ try {
410
+ const agentDetail = await ctx.marketSDK.agents.getAgentDetail(inputIdentifier);
411
+ log('Agent detail for ownership check: ownerId=%s', agentDetail?.ownerId);
412
+
413
+ // Get Market user info to get accountId (Market's user ID)
414
+ // Support both trustedClientToken and OIDC accessToken authentication
415
+ const userInfo = ctx.marketUserInfo as TrustedClientUserInfo | undefined;
416
+ const accessToken = (ctx as { marketOidcAccessToken?: string }).marketOidcAccessToken;
417
+ let currentAccountId: number | null = null;
418
+
419
+ const marketUserInfoResult = await fetchMarketUserInfo({ accessToken, userInfo });
420
+ currentAccountId = marketUserInfoResult?.accountId ?? null;
421
+ log('Market user info: accountId=%s', currentAccountId);
422
+
423
+ const ownerId = agentDetail?.ownerId;
424
+
425
+ log('Ownership check: currentAccountId=%s, ownerId=%s', currentAccountId, ownerId);
426
+
427
+ if (!currentAccountId || `${ownerId}` !== `${currentAccountId}`) {
428
+ // Not the owner, need to create a new agent
429
+ log('User is not owner, will create new agent');
430
+ finalIdentifier = undefined;
431
+ isNewAgent = true;
432
+ }
433
+ } catch (detailError) {
434
+ // Agent not found or error, create new
435
+ log('Agent not found or error, will create new: %O', detailError);
436
+ finalIdentifier = undefined;
437
+ isNewAgent = true;
438
+ }
439
+ } else {
440
+ isNewAgent = true;
441
+ }
442
+
443
+ // Step 2: Create new agent if needed
444
+ if (!finalIdentifier) {
445
+ // Generate a unique 8-character identifier
446
+ finalIdentifier = generateMarketIdentifier();
447
+ isNewAgent = true;
448
+
449
+ log('Creating new agent with identifier: %s', finalIdentifier);
450
+
451
+ await ctx.marketSDK.agents.createAgent({
452
+ identifier: finalIdentifier,
453
+ name,
454
+ });
455
+ }
456
+
457
+ // Step 3: Create version for the agent
458
+ log('Creating version for agent: %s', finalIdentifier);
459
+
460
+ await ctx.marketSDK.agents.createAgentVersion({
461
+ ...versionData,
462
+ identifier: finalIdentifier,
463
+ name,
464
+ });
465
+
466
+ return {
467
+ identifier: finalIdentifier,
468
+ isNewAgent,
469
+ success: true,
470
+ };
471
+ } catch (error) {
472
+ log('Error in publishOrCreate: %O', error);
473
+ throw new TRPCError({
474
+ cause: error,
475
+ code: 'INTERNAL_SERVER_ERROR',
476
+ message: error instanceof Error ? error.message : 'Failed to publish agent',
477
+ });
478
+ }
479
+ }),
480
+
481
+ /**
482
+ * Unpublish an agent (hide from marketplace, can be republished)
483
+ * POST /market/agent/:identifier/unpublish
484
+ */
485
+ unpublishAgent: agentProcedure
486
+ .input(z.object({ identifier: z.string() }))
487
+ .mutation(async ({ input, ctx }) => {
488
+ log('unpublishAgent input: %O', input);
489
+
490
+ try {
491
+ const response = await ctx.marketSDK.agents.unpublish(input.identifier);
492
+ return response ?? { success: true };
493
+ } catch (error) {
494
+ log('Error unpublishing agent: %O', error);
495
+ throw new TRPCError({
496
+ cause: error,
497
+ code: 'INTERNAL_SERVER_ERROR',
498
+ message: error instanceof Error ? error.message : 'Failed to unpublish agent',
499
+ });
500
+ }
501
+ }),
502
+ });
503
+
504
+ export type AgentRouter = typeof agentRouter;
@@ -16,6 +16,11 @@ import {
16
16
  ProviderSorts,
17
17
  } from '@/types/discover';
18
18
 
19
+ import { agentRouter } from './agent';
20
+ import { oidcRouter } from './oidc';
21
+ import { socialRouter } from './social';
22
+ import { userRouter } from './user';
23
+
19
24
  const log = debug('lambda-router:market');
20
25
 
21
26
  const marketSourceSchema = z.enum(['legacy', 'new']);
@@ -36,6 +41,9 @@ const marketProcedure = publicProcedure
36
41
  });
37
42
 
38
43
  export const marketRouter = router({
44
+ // ============================== Agent Management (authenticated) ==============================
45
+ agent: agentRouter,
46
+
39
47
  // ============================== Assistant Market ==============================
40
48
  getAssistantCategories: marketProcedure
41
49
  .input(
@@ -516,6 +524,9 @@ export const marketRouter = router({
516
524
  }
517
525
  }),
518
526
 
527
+ // ============================== OIDC Authentication ==============================
528
+ oidc: oidcRouter,
529
+
519
530
  registerClientInMarketplace: marketProcedure.input(z.object({})).mutation(async ({ ctx }) => {
520
531
  return ctx.discoverService.registerClient({
521
532
  userAgent: ctx.userAgent,
@@ -671,4 +682,10 @@ export const marketRouter = router({
671
682
  return { success: false };
672
683
  }
673
684
  }),
685
+
686
+ // ============================== Social Features ==============================
687
+ social: socialRouter,
688
+
689
+ // ============================== User Profile ==============================
690
+ user: userRouter,
674
691
  });
@@ -0,0 +1,169 @@
1
+ import { MarketSDK } from '@lobehub/market-sdk';
2
+ import { TRPCError } from '@trpc/server';
3
+ import debug from 'debug';
4
+ import { z } from 'zod';
5
+
6
+ import { publicProcedure, router } from '@/libs/trpc/lambda';
7
+ import { marketUserInfo, serverDatabase } from '@/libs/trpc/lambda/middleware';
8
+ import { generateTrustedClientToken } from '@/libs/trusted-client';
9
+
10
+ const log = debug('lambda-router:market:oidc');
11
+
12
+ const MARKET_BASE_URL = process.env.NEXT_PUBLIC_MARKET_BASE_URL || 'https://market.lobehub.com';
13
+
14
+ // OIDC procedures are public (used during authentication flow)
15
+ const oidcProcedure = publicProcedure.use(serverDatabase).use(marketUserInfo);
16
+
17
+ export const oidcRouter = router({
18
+ /**
19
+ * Exchange OAuth code for tokens
20
+ * POST /market/oidc/token (with grant_type=authorization_code)
21
+ */
22
+ exchangeAuthorizationCode: oidcProcedure
23
+ .input(
24
+ z.object({
25
+ clientId: z.string(),
26
+ code: z.string(),
27
+ codeVerifier: z.string(),
28
+ redirectUri: z.string(),
29
+ }),
30
+ )
31
+ .mutation(async ({ input }) => {
32
+ log('exchangeAuthorizationCode input: %O', { ...input, code: '[REDACTED]' });
33
+
34
+ const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
35
+
36
+ try {
37
+ const response = await market.auth.exchangeOAuthToken({
38
+ clientId: input.clientId,
39
+ code: input.code,
40
+ codeVerifier: input.codeVerifier,
41
+ grantType: 'authorization_code',
42
+ redirectUri: input.redirectUri,
43
+ });
44
+ return response;
45
+ } catch (error) {
46
+ log('Error exchanging authorization code: %O', error);
47
+ throw new TRPCError({
48
+ cause: error,
49
+ code: 'INTERNAL_SERVER_ERROR',
50
+ message: error instanceof Error ? error.message : 'Failed to exchange authorization code',
51
+ });
52
+ }
53
+ }),
54
+
55
+ /**
56
+ * Get OAuth handoff information
57
+ * GET /market/oidc/handoff?id=xxx
58
+ */
59
+ getOAuthHandoff: oidcProcedure.input(z.object({ id: z.string() })).query(async ({ input }) => {
60
+ log('getOAuthHandoff input: %O', input);
61
+
62
+ const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
63
+
64
+ try {
65
+ const handoff = await market.auth.getOAuthHandoff(input.id);
66
+ return handoff;
67
+ } catch (error) {
68
+ log('Error getting OAuth handoff: %O', error);
69
+ throw new TRPCError({
70
+ cause: error,
71
+ code: 'INTERNAL_SERVER_ERROR',
72
+ message: error instanceof Error ? error.message : 'Failed to get OAuth handoff',
73
+ });
74
+ }
75
+ }),
76
+
77
+ /**
78
+ * Get user info from token or trusted client
79
+ * POST /market/oidc/userinfo
80
+ */
81
+ getUserInfo: oidcProcedure
82
+ .input(z.object({ token: z.string().optional() }))
83
+ .mutation(async ({ input, ctx }) => {
84
+ log('getUserInfo input: token=%s', input.token ? '[REDACTED]' : 'undefined');
85
+
86
+ const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
87
+
88
+ try {
89
+ // If token is provided, use it
90
+ if (input.token) {
91
+ const response = await market.auth.getUserInfo(input.token);
92
+ return response;
93
+ }
94
+
95
+ // Otherwise, try to use trustedClientToken
96
+ if (ctx.marketUserInfo) {
97
+ const trustedClientToken = generateTrustedClientToken(ctx.marketUserInfo);
98
+
99
+ if (trustedClientToken) {
100
+ const userInfoUrl = `${MARKET_BASE_URL}/lobehub-oidc/userinfo`;
101
+ const response = await fetch(userInfoUrl, {
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ 'x-lobe-trust-token': trustedClientToken,
105
+ },
106
+ method: 'GET',
107
+ });
108
+
109
+ if (!response.ok) {
110
+ throw new Error(
111
+ `Failed to fetch user info: ${response.status} ${response.statusText}`,
112
+ );
113
+ }
114
+
115
+ return await response.json();
116
+ }
117
+ }
118
+
119
+ throw new TRPCError({
120
+ code: 'BAD_REQUEST',
121
+ message: 'Token is required for userinfo',
122
+ });
123
+ } catch (error) {
124
+ if (error instanceof TRPCError) throw error;
125
+
126
+ log('Error getting user info: %O', error);
127
+ throw new TRPCError({
128
+ cause: error,
129
+ code: 'INTERNAL_SERVER_ERROR',
130
+ message: error instanceof Error ? error.message : 'Failed to get user info',
131
+ });
132
+ }
133
+ }),
134
+
135
+ /**
136
+ * Refresh access token
137
+ * POST /market/oidc/token (with grant_type=refresh_token)
138
+ */
139
+ refreshToken: oidcProcedure
140
+ .input(
141
+ z.object({
142
+ clientId: z.string().optional(),
143
+ refreshToken: z.string(),
144
+ }),
145
+ )
146
+ .mutation(async ({ input }) => {
147
+ log('refreshToken input: %O', { ...input, refreshToken: '[REDACTED]' });
148
+
149
+ const market = new MarketSDK({ baseURL: MARKET_BASE_URL });
150
+
151
+ try {
152
+ const response = await market.auth.exchangeOAuthToken({
153
+ clientId: input.clientId,
154
+ grantType: 'refresh_token',
155
+ refreshToken: input.refreshToken,
156
+ });
157
+ return response;
158
+ } catch (error) {
159
+ log('Error refreshing token: %O', error);
160
+ throw new TRPCError({
161
+ cause: error,
162
+ code: 'INTERNAL_SERVER_ERROR',
163
+ message: error instanceof Error ? error.message : 'Failed to refresh token',
164
+ });
165
+ }
166
+ }),
167
+ });
168
+
169
+ export type OidcRouter = typeof oidcRouter;