@lobehub/chat 1.99.6 → 1.100.1

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 (108) hide show
  1. package/.cursor/rules/testing-guide/testing-guide.mdc +173 -0
  2. package/.github/workflows/desktop-pr-build.yml +3 -3
  3. package/.github/workflows/release-desktop-beta.yml +3 -3
  4. package/CHANGELOG.md +50 -0
  5. package/apps/desktop/package.json +5 -2
  6. package/apps/desktop/src/main/controllers/AuthCtr.ts +310 -111
  7. package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +1 -1
  8. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +50 -3
  9. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +188 -23
  10. package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +37 -18
  11. package/apps/desktop/src/main/types/store.ts +1 -0
  12. package/apps/desktop/src/preload/electronApi.ts +2 -1
  13. package/apps/desktop/src/preload/streamer.ts +58 -0
  14. package/changelog/v1.json +18 -0
  15. package/docs/development/database-schema.dbml +9 -0
  16. package/docs/self-hosting/environment-variables/model-provider.mdx +25 -0
  17. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +25 -0
  18. package/docs/self-hosting/faq/vercel-ai-image-timeout.mdx +65 -0
  19. package/docs/self-hosting/faq/vercel-ai-image-timeout.zh-CN.mdx +63 -0
  20. package/docs/usage/providers/fal.mdx +6 -6
  21. package/docs/usage/providers/fal.zh-CN.mdx +6 -6
  22. package/locales/ar/electron.json +3 -0
  23. package/locales/ar/oauth.json +8 -4
  24. package/locales/bg-BG/electron.json +3 -0
  25. package/locales/bg-BG/oauth.json +8 -4
  26. package/locales/de-DE/electron.json +3 -0
  27. package/locales/de-DE/oauth.json +9 -5
  28. package/locales/en-US/electron.json +3 -0
  29. package/locales/en-US/oauth.json +8 -4
  30. package/locales/es-ES/electron.json +3 -0
  31. package/locales/es-ES/oauth.json +9 -5
  32. package/locales/fa-IR/electron.json +3 -0
  33. package/locales/fa-IR/oauth.json +8 -4
  34. package/locales/fr-FR/electron.json +3 -0
  35. package/locales/fr-FR/oauth.json +8 -4
  36. package/locales/it-IT/electron.json +3 -0
  37. package/locales/it-IT/oauth.json +9 -5
  38. package/locales/ja-JP/electron.json +3 -0
  39. package/locales/ja-JP/oauth.json +8 -4
  40. package/locales/ko-KR/electron.json +3 -0
  41. package/locales/ko-KR/oauth.json +8 -4
  42. package/locales/nl-NL/electron.json +3 -0
  43. package/locales/nl-NL/oauth.json +9 -5
  44. package/locales/pl-PL/electron.json +3 -0
  45. package/locales/pl-PL/oauth.json +8 -4
  46. package/locales/pt-BR/electron.json +3 -0
  47. package/locales/pt-BR/oauth.json +8 -4
  48. package/locales/ru-RU/electron.json +3 -0
  49. package/locales/ru-RU/oauth.json +8 -4
  50. package/locales/tr-TR/electron.json +3 -0
  51. package/locales/tr-TR/oauth.json +8 -4
  52. package/locales/vi-VN/electron.json +3 -0
  53. package/locales/vi-VN/oauth.json +9 -5
  54. package/locales/zh-CN/electron.json +3 -0
  55. package/locales/zh-CN/oauth.json +8 -4
  56. package/locales/zh-TW/electron.json +3 -0
  57. package/locales/zh-TW/oauth.json +8 -4
  58. package/package.json +3 -3
  59. package/packages/electron-client-ipc/src/dispatch.ts +14 -2
  60. package/packages/electron-client-ipc/src/index.ts +1 -0
  61. package/packages/electron-client-ipc/src/streamInvoke.ts +62 -0
  62. package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +5 -0
  63. package/packages/electron-client-ipc/src/utils/headers.ts +27 -0
  64. package/packages/electron-client-ipc/src/utils/request.ts +28 -0
  65. package/src/app/(backend)/oidc/callback/desktop/route.ts +58 -0
  66. package/src/app/(backend)/oidc/handoff/route.ts +46 -0
  67. package/src/app/[variants]/oauth/callback/error/page.tsx +55 -0
  68. package/src/app/[variants]/oauth/callback/layout.tsx +12 -0
  69. package/src/app/[variants]/oauth/callback/loading.tsx +3 -0
  70. package/src/app/[variants]/oauth/{consent/[uid] → callback}/success/page.tsx +10 -1
  71. package/src/app/[variants]/oauth/consent/[uid]/Consent.tsx +7 -1
  72. package/src/database/client/migrations.json +8 -0
  73. package/src/database/migrations/0028_oauth_handoffs.sql +8 -0
  74. package/src/database/migrations/meta/0028_snapshot.json +6055 -0
  75. package/src/database/migrations/meta/_journal.json +7 -0
  76. package/src/database/models/oauthHandoff.ts +94 -0
  77. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  78. package/src/database/schemas/oidc.ts +46 -0
  79. package/src/features/ElectronTitlebar/Connection/Waiting.tsx +59 -115
  80. package/src/features/ElectronTitlebar/Connection/WaitingAnim.tsx +114 -0
  81. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +1 -1
  82. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +2 -1
  83. package/src/libs/oidc-provider/config.ts +16 -17
  84. package/src/libs/oidc-provider/jwt.ts +135 -0
  85. package/src/libs/oidc-provider/provider.ts +22 -38
  86. package/src/libs/trpc/client/async.ts +1 -2
  87. package/src/libs/trpc/client/edge.ts +1 -2
  88. package/src/libs/trpc/client/lambda.ts +1 -1
  89. package/src/libs/trpc/client/tools.ts +1 -2
  90. package/src/libs/trpc/lambda/context.ts +9 -16
  91. package/src/locales/default/electron.ts +3 -0
  92. package/src/locales/default/oauth.ts +8 -4
  93. package/src/middleware.ts +10 -4
  94. package/src/server/globalConfig/genServerAiProviderConfig.test.ts +235 -0
  95. package/src/server/globalConfig/genServerAiProviderConfig.ts +9 -10
  96. package/src/server/services/oidc/index.ts +0 -71
  97. package/src/services/chat.ts +5 -1
  98. package/src/services/electron/remoteServer.ts +0 -7
  99. package/src/store/aiInfra/slices/aiProvider/action.ts +2 -1
  100. package/src/{libs/trpc/client/helpers → utils/electron}/desktopRemoteRPCFetch.ts +22 -7
  101. package/src/utils/getFallbackModelProperty.test.ts +193 -0
  102. package/src/utils/getFallbackModelProperty.ts +36 -0
  103. package/src/utils/parseModels.test.ts +150 -48
  104. package/src/utils/parseModels.ts +26 -11
  105. package/src/utils/server/auth.ts +22 -0
  106. package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +0 -36
  107. package/src/app/[variants]/oauth/handoff/Client.tsx +0 -98
  108. package/src/app/[variants]/oauth/handoff/page.tsx +0 -13
@@ -0,0 +1,135 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import debug from 'debug';
3
+ import { importJWK, jwtVerify } from 'jose';
4
+
5
+ import { oidcEnv } from '@/envs/oidc';
6
+
7
+ const log = debug('oidc-jwt');
8
+
9
+ /**
10
+ * 从环境变量中获取 JWKS
11
+ * 该 JWKS 是一个包含 RS256 私钥的 JSON 对象
12
+ */
13
+ export const getJWKS = (): object => {
14
+ try {
15
+ const jwksString = oidcEnv.OIDC_JWKS_KEY;
16
+
17
+ if (!jwksString) {
18
+ throw new Error(
19
+ 'OIDC_JWKS_KEY 环境变量是必需的。请使用 scripts/generate-oidc-jwk.mjs 生成 JWKS。',
20
+ );
21
+ }
22
+
23
+ // 尝试解析 JWKS JSON 字符串
24
+ const jwks = JSON.parse(jwksString);
25
+
26
+ // 检查 JWKS 格式是否正确
27
+ if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
28
+ throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
29
+ }
30
+
31
+ // 检查是否有 RS256 算法的密钥
32
+ const hasRS256Key = jwks.keys.some((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
33
+ if (!hasRS256Key) {
34
+ throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
35
+ }
36
+
37
+ return jwks;
38
+ } catch (error) {
39
+ console.error('解析 JWKS 失败:', error);
40
+ throw new Error(`OIDC_JWKS_KEY 解析错误: ${(error as Error).message}`);
41
+ }
42
+ };
43
+
44
+ /**
45
+ * 从环境变量中获取 JWKS 并提取第一个 RSA 密钥
46
+ */
47
+ const getJWKSPublicKey = async () => {
48
+ try {
49
+ const jwksString = oidcEnv.OIDC_JWKS_KEY;
50
+
51
+ if (!jwksString) {
52
+ throw new Error('OIDC_JWKS_KEY 环境变量未设置');
53
+ }
54
+
55
+ const jwks = JSON.parse(jwksString);
56
+
57
+ if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
58
+ throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
59
+ }
60
+
61
+ // 查找 RS256 算法的 RSA 密钥
62
+ const rsaKey = jwks.keys.find((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
63
+
64
+ if (!rsaKey) {
65
+ throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
66
+ }
67
+
68
+ // 导入 JWK 为公钥
69
+ const publicKey = await importJWK(rsaKey, 'RS256');
70
+
71
+ return publicKey;
72
+ } catch (error) {
73
+ log('获取 JWKS 公钥失败: %O', error);
74
+ throw new Error(`JWKS 公钥获取失败: ${(error as Error).message}`);
75
+ }
76
+ };
77
+
78
+ /**
79
+ * 验证 OIDC JWT Access Token
80
+ * @param token - JWT access token
81
+ * @returns 解析后的 token payload 和用户信息
82
+ */
83
+ export const validateOIDCJWT = async (token: string) => {
84
+ try {
85
+ log('开始验证 OIDC JWT token');
86
+
87
+ // 获取公钥
88
+ const publicKey = await getJWKSPublicKey();
89
+
90
+ // 验证 JWT
91
+ const { payload } = await jwtVerify(token, publicKey, {
92
+ algorithms: ['RS256'],
93
+ // 可以添加其他验证选项,如 issuer、audience 等
94
+ });
95
+
96
+ log('JWT 验证成功,payload: %O', payload);
97
+
98
+ // 提取用户信息
99
+ const userId = payload.sub;
100
+ const clientId = payload.aud;
101
+
102
+ if (!userId) {
103
+ throw new TRPCError({
104
+ code: 'UNAUTHORIZED',
105
+ message: 'JWT token 中缺少用户 ID (sub)',
106
+ });
107
+ }
108
+
109
+ return {
110
+ clientId,
111
+ payload,
112
+ tokenData: {
113
+ aud: clientId,
114
+ client_id: clientId,
115
+ exp: payload.exp,
116
+ iat: payload.iat,
117
+ jti: payload.jti,
118
+ scope: payload.scope,
119
+ sub: userId,
120
+ },
121
+ userId,
122
+ };
123
+ } catch (error) {
124
+ if (error instanceof TRPCError) {
125
+ throw error;
126
+ }
127
+
128
+ log('JWT 验证失败: %O', error);
129
+
130
+ throw new TRPCError({
131
+ code: 'UNAUTHORIZED',
132
+ message: `JWT token 验证失败: ${(error as Error).message}`,
133
+ });
134
+ }
135
+ };
@@ -1,12 +1,12 @@
1
1
  import debug from 'debug';
2
- import Provider, { Configuration, KoaContextWithOIDC } from 'oidc-provider';
2
+ import Provider, { Configuration, KoaContextWithOIDC, errors } from 'oidc-provider';
3
3
  import urlJoin from 'url-join';
4
4
 
5
5
  import { serverDBEnv } from '@/config/db';
6
6
  import { UserModel } from '@/database/models/user';
7
7
  import { LobeChatDatabase } from '@/database/type';
8
8
  import { appEnv } from '@/envs/app';
9
- import { oidcEnv } from '@/envs/oidc';
9
+ import { getJWKS } from '@/libs/oidc-provider/jwt';
10
10
 
11
11
  import { DrizzleAdapter } from './adapter';
12
12
  import { defaultClaims, defaultClients, defaultScopes } from './config';
@@ -14,40 +14,7 @@ import { createInteractionPolicy } from './interaction-policy';
14
14
 
15
15
  const logProvider = debug('lobe-oidc:provider'); // <--- 添加 provider 日志实例
16
16
 
17
- /**
18
- * 从环境变量中获取 JWKS
19
- * 该 JWKS 是一个包含 RS256 私钥的 JSON 对象
20
- */
21
- const getJWKS = (): object => {
22
- try {
23
- const jwksString = oidcEnv.OIDC_JWKS_KEY;
24
-
25
- if (!jwksString) {
26
- throw new Error(
27
- 'OIDC_JWKS_KEY 环境变量是必需的。请使用 scripts/generate-oidc-jwk.mjs 生成 JWKS。',
28
- );
29
- }
30
-
31
- // 尝试解析 JWKS JSON 字符串
32
- const jwks = JSON.parse(jwksString);
33
-
34
- // 检查 JWKS 格式是否正确
35
- if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
36
- throw new Error('JWKS 格式无效: 缺少或为空的 keys 数组');
37
- }
38
-
39
- // 检查是否有 RS256 算法的密钥
40
- const hasRS256Key = jwks.keys.some((key: any) => key.alg === 'RS256' && key.kty === 'RSA');
41
- if (!hasRS256Key) {
42
- throw new Error('JWKS 中没有找到 RS256 算法的 RSA 密钥');
43
- }
44
-
45
- return jwks;
46
- } catch (error) {
47
- console.error('解析 JWKS 失败:', error);
48
- throw new Error(`OIDC_JWKS_KEY 解析错误: ${(error as Error).message}`);
49
- }
50
- };
17
+ export const API_AUDIENCE = 'urn:lobehub:chat'; // <-- 把这里换成你自己的 API 标识符
51
18
 
52
19
  /**
53
20
  * 获取 Cookie 密钥,使用 KEY_VAULTS_SECRET
@@ -123,7 +90,24 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
123
90
  devInteractions: { enabled: false },
124
91
  deviceFlow: { enabled: false },
125
92
  introspection: { enabled: true },
126
- resourceIndicators: { enabled: false },
93
+ resourceIndicators: {
94
+ defaultResource: () => API_AUDIENCE,
95
+ enabled: true,
96
+ getResourceServerInfo: (ctx, resourceIndicator) => {
97
+ logProvider('getResourceServerInfo called with indicator: %s', resourceIndicator); // <-- 添加这行日志
98
+ if (resourceIndicator === API_AUDIENCE) {
99
+ logProvider('Indicator matches API_AUDIENCE, returning JWT config.'); // <-- 添加这行日志
100
+ return {
101
+ accessTokenFormat: 'jwt',
102
+ audience: API_AUDIENCE,
103
+ scope: ctx.oidc.client?.scope || 'read',
104
+ };
105
+ }
106
+
107
+ logProvider('Indicator does not match API_AUDIENCE, throwing InvalidTarget.'); // <-- 添加这行日志
108
+ throw new errors.InvalidTarget();
109
+ },
110
+ },
127
111
  revocation: { enabled: true },
128
112
  rpInitiatedLogout: { enabled: true },
129
113
  userinfo: { enabled: true },
@@ -256,7 +240,7 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
256
240
 
257
241
  // 8. 令牌有效期
258
242
  ttl: {
259
- AccessToken: 7 * 24 * 60 * 60, // 1 week temporarily,need to revert 1 hour with better implement
243
+ AccessToken: 25 * 3600, // 25 hour
260
244
  AuthorizationCode: 600, // 10 minutes
261
245
  DeviceCode: 600, // 10 minutes (if enabled)
262
246
 
@@ -3,8 +3,7 @@ import superjson from 'superjson';
3
3
 
4
4
  import { isDesktop } from '@/const/version';
5
5
  import { AsyncRouter } from '@/server/routers/async';
6
-
7
- import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
6
+ import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
8
7
 
9
8
  export const asyncClient = createTRPCClient<AsyncRouter>({
10
9
  links: [
@@ -4,8 +4,7 @@ import superjson from 'superjson';
4
4
  import { isDesktop } from '@/const/version';
5
5
  import type { EdgeRouter } from '@/server/routers/edge';
6
6
  import { withBasePath } from '@/utils/basePath';
7
-
8
- import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
7
+ import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
9
8
 
10
9
  export const edgeClient = createTRPCClient<EdgeRouter>({
11
10
  links: [
@@ -15,7 +15,7 @@ const links = [
15
15
  httpBatchLink({
16
16
  fetch: async (input, init) => {
17
17
  if (isDesktop) {
18
- const { desktopRemoteRPCFetch } = await import('./helpers/desktopRemoteRPCFetch');
18
+ const { desktopRemoteRPCFetch } = await import('@/utils/electron/desktopRemoteRPCFetch');
19
19
 
20
20
  const res = await desktopRemoteRPCFetch(input as string, init);
21
21
 
@@ -3,8 +3,7 @@ import superjson from 'superjson';
3
3
 
4
4
  import { isDesktop } from '@/const/version';
5
5
  import type { ToolsRouter } from '@/server/routers/tools';
6
-
7
- import { fetchWithDesktopRemoteRPC } from './helpers/desktopRemoteRPCFetch';
6
+ import { fetchWithDesktopRemoteRPC } from '@/utils/electron/desktopRemoteRPCFetch';
8
7
 
9
8
  export const toolsClient = createTRPCClient<ToolsRouter>({
10
9
  links: [
@@ -6,7 +6,7 @@ import { NextRequest } from 'next/server';
6
6
  import { JWTPayload, LOBE_CHAT_AUTH_HEADER, enableClerk, enableNextAuth } from '@/const/auth';
7
7
  import { oidcEnv } from '@/envs/oidc';
8
8
  import { ClerkAuth, IClerkAuth } from '@/libs/clerk-auth';
9
- import { extractBearerToken } from '@/utils/server/auth';
9
+ import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
10
10
 
11
11
  // Create context logger namespace
12
12
  const log = debug('lobe-trpc:lambda:context');
@@ -98,26 +98,19 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
98
98
  let auth;
99
99
  let oidcAuth = null;
100
100
 
101
- // Prioritize checking the standard Authorization header for OIDC Bearer Token validation
101
+ // Prioritize checking for OIDC authentication (both standard Authorization and custom Oidc-Auth headers)
102
102
  if (oidcEnv.ENABLE_OIDC) {
103
103
  log('OIDC enabled, attempting OIDC authentication');
104
104
  const standardAuthorization = request.headers.get('Authorization');
105
+ const oidcAuthToken = request.headers.get('Oidc-Auth');
105
106
  log('Standard Authorization header: %s', standardAuthorization ? 'exists' : 'not found');
107
+ log('Oidc-Auth header: %s', oidcAuthToken ? 'exists' : 'not found');
106
108
 
107
109
  try {
108
- // Use extractBearerToken from utils
109
- const bearerToken = extractBearerToken(standardAuthorization);
110
-
111
- log('Extracted Bearer Token: %s', bearerToken ? 'valid' : 'invalid');
112
- if (bearerToken) {
113
- const { OIDCService } = await import('@/server/services/oidc');
114
-
115
- // Initialize OIDC service
116
- log('Initializing OIDC service');
117
- const oidcService = await OIDCService.initialize();
118
- // Validate token using OIDCService
119
- log('Validating OIDC token');
120
- const tokenInfo = await oidcService.validateToken(bearerToken);
110
+ if (oidcAuthToken) {
111
+ // Use direct JWT validation instead of database lookup
112
+ const tokenInfo = await validateOIDCJWT(oidcAuthToken);
113
+
121
114
  oidcAuth = {
122
115
  payload: tokenInfo.tokenData,
123
116
  ...tokenInfo.tokenData, // Spread payload into oidcAuth
@@ -136,7 +129,7 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
136
129
  }
137
130
  } catch (error) {
138
131
  // If OIDC authentication fails, log error and continue with other authentication methods
139
- if (standardAuthorization?.startsWith('Bearer ')) {
132
+ if (oidcAuthToken) {
140
133
  log('OIDC authentication failed, error: %O', error);
141
134
  console.error('OIDC authentication failed, trying other methods:', error);
142
135
  }
@@ -101,7 +101,10 @@ const electron = {
101
101
  waitingOAuth: {
102
102
  cancel: '取消',
103
103
  description: '浏览器已打开授权页面,请在浏览器中完成授权',
104
+ error: '授权失败: {{error}}',
105
+ errorTitle: '授权连接失败',
104
106
  helpText: '如果浏览器没有自动打开,请点击取消后重新尝试',
107
+ retry: '重试',
105
108
  title: '等待授权连接',
106
109
  },
107
110
  };
@@ -29,10 +29,14 @@ const oauth = {
29
29
  },
30
30
  title: '授权 {{clientName}}',
31
31
  },
32
- failed: {
32
+ error: {
33
33
  backToHome: '返回首页',
34
- subTitle: '您已拒绝授权应用访问您的 LobeChat 账户',
35
- title: '授权被拒绝',
34
+ desc: 'OAuth 授权失败,失败原因:{{reason}}',
35
+ reason: {
36
+ internal_error: '服务端错误',
37
+ invalid_request: '无效的请求参数',
38
+ },
39
+ title: '授权失败',
36
40
  },
37
41
  handoff: {
38
42
  desc: {
@@ -51,7 +55,7 @@ const oauth = {
51
55
  userWelcome: '欢迎回来,',
52
56
  },
53
57
  success: {
54
- subTitle: '您已成功授权应用访问您的 LobeChat 账户,可以关闭该页面了',
58
+ subTitle: '您已成功授权应用访问您的账户,可以关闭该页面了',
55
59
  title: '授权成功',
56
60
  },
57
61
  };
package/src/middleware.ts CHANGED
@@ -18,9 +18,9 @@ import { OAUTH_AUTHORIZED } from './const/auth';
18
18
  import { oidcEnv } from './envs/oidc';
19
19
 
20
20
  // Create debug logger instances
21
- const logDefault = debug('lobe-middleware:default');
22
- const logNextAuth = debug('lobe-middleware:next-auth');
23
- const logClerk = debug('lobe-middleware:clerk');
21
+ const logDefault = debug('middleware:default');
22
+ const logNextAuth = debug('middleware:next-auth');
23
+ const logClerk = debug('middleware:clerk');
24
24
 
25
25
  // OIDC session pre-sync constant
26
26
  const OIDC_SESSION_HEADER = 'x-oidc-session-sync';
@@ -146,13 +146,19 @@ const defaultMiddleware = (request: NextRequest) => {
146
146
  };
147
147
 
148
148
  const isPublicRoute = createRouteMatcher([
149
+ // backend api
149
150
  '/api/auth(.*)',
151
+ '/api/webhooks(.*)',
152
+ '/webapi(.*)',
150
153
  '/trpc(.*)',
151
154
  // next auth
152
155
  '/next-auth/(.*)',
153
156
  // clerk
154
157
  '/login',
155
158
  '/signup',
159
+ // oauth
160
+ '/oidc/handoff',
161
+ '/oidc/token',
156
162
  ]);
157
163
 
158
164
  const isProtectedRoute = createRouteMatcher([
@@ -164,7 +170,7 @@ const isProtectedRoute = createRouteMatcher([
164
170
  ]);
165
171
 
166
172
  // Initialize an Edge compatible NextAuth middleware
167
- const nextAuthMiddleware = NextAuthEdge.auth((req) => {
173
+ const nextAuthMiddleware = NextAuthEdge.auth(async (req) => {
168
174
  logNextAuth('NextAuth middleware processing request: %s %s', req.method, req.url);
169
175
 
170
176
  const response = defaultMiddleware(req);
@@ -0,0 +1,235 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { ModelProvider } from '@/libs/model-runtime';
4
+ import { AiFullModelCard } from '@/types/aiModel';
5
+
6
+ import { genServerAiProvidersConfig } from './genServerAiProviderConfig';
7
+
8
+ // Mock dependencies using importOriginal to preserve real provider data
9
+ vi.mock('@/config/aiModels', async (importOriginal) => {
10
+ const actual = await importOriginal<typeof import('@/config/aiModels')>();
11
+ return {
12
+ ...actual,
13
+ // Keep the original exports but we can override specific ones if needed
14
+ };
15
+ });
16
+
17
+ vi.mock('@/config/llm', () => ({
18
+ getLLMConfig: vi.fn(() => ({
19
+ ENABLED_OPENAI: true,
20
+ ENABLED_ANTHROPIC: false,
21
+ ENABLED_AI21: false,
22
+ })),
23
+ }));
24
+
25
+ vi.mock('@/utils/parseModels', () => ({
26
+ extractEnabledModels: vi.fn((providerId: string, modelString?: string) => {
27
+ if (!modelString) return undefined;
28
+ return [`${providerId}-model-1`, `${providerId}-model-2`];
29
+ }),
30
+ transformToAiModelList: vi.fn((params) => {
31
+ return params.defaultModels;
32
+ }),
33
+ }));
34
+
35
+ describe('genServerAiProvidersConfig', () => {
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ // Clear environment variables
39
+ Object.keys(process.env).forEach((key) => {
40
+ if (key.includes('MODEL_LIST')) {
41
+ delete process.env[key];
42
+ }
43
+ });
44
+ });
45
+
46
+ it('should generate basic provider config with default settings', () => {
47
+ const result = genServerAiProvidersConfig({});
48
+
49
+ expect(result).toHaveProperty('openai');
50
+ expect(result).toHaveProperty('anthropic');
51
+
52
+ expect(result.openai).toEqual({
53
+ enabled: true,
54
+ enabledModels: undefined,
55
+ serverModelLists: expect.any(Array),
56
+ });
57
+
58
+ expect(result.anthropic).toEqual({
59
+ enabled: false,
60
+ enabledModels: undefined,
61
+ serverModelLists: expect.any(Array),
62
+ });
63
+ });
64
+
65
+ it('should use custom enabled settings from specificConfig', () => {
66
+ const specificConfig = {
67
+ openai: {
68
+ enabled: false,
69
+ },
70
+ anthropic: {
71
+ enabled: true,
72
+ },
73
+ };
74
+
75
+ const result = genServerAiProvidersConfig(specificConfig);
76
+
77
+ expect(result.openai.enabled).toBe(false);
78
+ expect(result.anthropic.enabled).toBe(true);
79
+ });
80
+
81
+ it('should use custom enabledKey from specificConfig', async () => {
82
+ const specificConfig = {
83
+ openai: {
84
+ enabledKey: 'CUSTOM_OPENAI_ENABLED',
85
+ },
86
+ };
87
+
88
+ // Mock the LLM config to include our custom key
89
+ const { getLLMConfig } = vi.mocked(await import('@/config/llm'));
90
+ getLLMConfig.mockReturnValue({
91
+ ENABLED_OPENAI: true,
92
+ ENABLED_ANTHROPIC: false,
93
+ CUSTOM_OPENAI_ENABLED: true,
94
+ } as any);
95
+
96
+ const result = genServerAiProvidersConfig(specificConfig);
97
+
98
+ expect(result.openai.enabled).toBe(true);
99
+ });
100
+
101
+ it('should use environment variables for model lists', async () => {
102
+ process.env.OPENAI_MODEL_LIST = '+gpt-4,+gpt-3.5-turbo';
103
+
104
+ const { extractEnabledModels } = vi.mocked(await import('@/utils/parseModels'));
105
+ extractEnabledModels.mockReturnValue(['gpt-4', 'gpt-3.5-turbo']);
106
+
107
+ const result = genServerAiProvidersConfig({});
108
+
109
+ expect(extractEnabledModels).toHaveBeenCalledWith('openai', '+gpt-4,+gpt-3.5-turbo', false);
110
+ expect(result.openai.enabledModels).toEqual(['gpt-4', 'gpt-3.5-turbo']);
111
+ });
112
+
113
+ it('should use custom modelListKey from specificConfig', async () => {
114
+ const specificConfig = {
115
+ openai: {
116
+ modelListKey: 'CUSTOM_OPENAI_MODELS',
117
+ },
118
+ };
119
+
120
+ process.env.CUSTOM_OPENAI_MODELS = '+custom-model';
121
+
122
+ const { extractEnabledModels } = vi.mocked(await import('@/utils/parseModels'));
123
+
124
+ genServerAiProvidersConfig(specificConfig);
125
+
126
+ expect(extractEnabledModels).toHaveBeenCalledWith('openai', '+custom-model', false);
127
+ });
128
+
129
+ it('should handle withDeploymentName option', async () => {
130
+ const specificConfig = {
131
+ openai: {
132
+ withDeploymentName: true,
133
+ },
134
+ };
135
+
136
+ process.env.OPENAI_MODEL_LIST = '+gpt-4->deployment1';
137
+
138
+ const { extractEnabledModels, transformToAiModelList } = vi.mocked(
139
+ await import('@/utils/parseModels'),
140
+ );
141
+
142
+ genServerAiProvidersConfig(specificConfig);
143
+
144
+ expect(extractEnabledModels).toHaveBeenCalledWith('openai', '+gpt-4->deployment1', true);
145
+ expect(transformToAiModelList).toHaveBeenCalledWith({
146
+ defaultModels: expect.any(Array),
147
+ modelString: '+gpt-4->deployment1',
148
+ providerId: 'openai',
149
+ withDeploymentName: true,
150
+ });
151
+ });
152
+
153
+ it('should include fetchOnClient when specified in config', () => {
154
+ const specificConfig = {
155
+ openai: {
156
+ fetchOnClient: true,
157
+ },
158
+ };
159
+
160
+ const result = genServerAiProvidersConfig(specificConfig);
161
+
162
+ expect(result.openai).toHaveProperty('fetchOnClient', true);
163
+ });
164
+
165
+ it('should not include fetchOnClient when not specified in config', () => {
166
+ const result = genServerAiProvidersConfig({});
167
+
168
+ expect(result.openai).not.toHaveProperty('fetchOnClient');
169
+ });
170
+
171
+ it('should handle all available providers', () => {
172
+ const result = genServerAiProvidersConfig({});
173
+
174
+ // Check that result includes some key providers
175
+ expect(result).toHaveProperty('openai');
176
+ expect(result).toHaveProperty('anthropic');
177
+
178
+ // Check structure for each provider
179
+ Object.keys(result).forEach((provider) => {
180
+ expect(result[provider]).toHaveProperty('enabled');
181
+ expect(result[provider]).toHaveProperty('serverModelLists');
182
+ // enabled can be boolean or undefined (when no config is provided)
183
+ expect(['boolean', 'undefined']).toContain(typeof result[provider].enabled);
184
+ expect(Array.isArray(result[provider].serverModelLists)).toBe(true);
185
+ });
186
+ });
187
+ });
188
+
189
+ describe('genServerAiProvidersConfig Error Handling', () => {
190
+ it('should throw error when a provider is not found in aiModels', async () => {
191
+ // Reset all mocks to create a clean test environment
192
+ vi.resetModules();
193
+
194
+ // Mock dependencies with a missing provider scenario
195
+ vi.doMock('@/config/aiModels', () => ({
196
+ // Explicitly set openai to undefined to simulate missing provider
197
+ openai: undefined,
198
+ anthropic: [
199
+ {
200
+ id: 'claude-3',
201
+ displayName: 'Claude 3',
202
+ type: 'chat',
203
+ enabled: true,
204
+ },
205
+ ],
206
+ }));
207
+
208
+ vi.doMock('@/config/llm', () => ({
209
+ getLLMConfig: vi.fn(() => ({})),
210
+ }));
211
+
212
+ vi.doMock('@/utils/parseModels', () => ({
213
+ extractEnabledModels: vi.fn(() => undefined),
214
+ transformToAiModelList: vi.fn(() => []),
215
+ }));
216
+
217
+ // Mock ModelProvider to include the missing provider
218
+ vi.doMock('@/libs/model-runtime', () => ({
219
+ ModelProvider: {
220
+ openai: 'openai', // This exists in enum
221
+ anthropic: 'anthropic', // This exists in both enum and aiModels
222
+ },
223
+ }));
224
+
225
+ // Import the function with the new mocks
226
+ const { genServerAiProvidersConfig } = await import(
227
+ './genServerAiProviderConfig?v=' + Date.now()
228
+ );
229
+
230
+ // This should throw because 'openai' is in ModelProvider but not in aiModels
231
+ expect(() => {
232
+ genServerAiProvidersConfig({});
233
+ }).toThrow();
234
+ });
235
+ });