@lobehub/lobehub 2.0.0-next.221 → 2.0.0-next.223

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 (130) hide show
  1. package/.github/workflows/claude-auto-testing.yml +6 -3
  2. package/.github/workflows/claude-dedupe-issues.yml +8 -1
  3. package/.github/workflows/claude-issue-triage.yml +8 -14
  4. package/.github/workflows/claude-translate-comments.yml +6 -3
  5. package/.github/workflows/claude-translator.yml +12 -14
  6. package/.github/workflows/claude.yml +10 -20
  7. package/.github/workflows/test.yml +26 -0
  8. package/CHANGELOG.md +58 -0
  9. package/changelog/v1.json +18 -0
  10. package/e2e/package.json +1 -1
  11. package/e2e/src/mocks/index.ts +2 -2
  12. package/e2e/src/steps/{discover → community}/detail-pages.steps.ts +8 -8
  13. package/e2e/src/steps/{discover → community}/interactions.steps.ts +4 -4
  14. package/locales/zh-CN/components.json +1 -0
  15. package/package.json +3 -3
  16. package/packages/const/src/index.ts +0 -1
  17. package/packages/memory-user-memory/package.json +1 -0
  18. package/packages/memory-user-memory/src/extractors/context.test.ts +3 -2
  19. package/packages/memory-user-memory/src/extractors/experience.test.ts +3 -2
  20. package/packages/memory-user-memory/src/extractors/identity.test.ts +23 -6
  21. package/packages/memory-user-memory/src/extractors/preference.test.ts +3 -2
  22. package/packages/memory-user-memory/vitest.config.ts +4 -0
  23. package/packages/model-runtime/src/providers/replicate/index.ts +1 -1
  24. package/packages/ssrf-safe-fetch/index.test.ts +2 -2
  25. package/packages/ssrf-safe-fetch/package.json +3 -2
  26. package/packages/types/src/serverConfig.ts +2 -0
  27. package/packages/utils/package.json +1 -1
  28. package/packages/utils/src/client/xor-obfuscation.test.ts +32 -32
  29. package/packages/utils/src/client/xor-obfuscation.ts +3 -4
  30. package/packages/utils/src/imageToBase64.ts +1 -1
  31. package/packages/utils/src/server/__tests__/auth.test.ts +1 -1
  32. package/packages/utils/src/server/auth.ts +1 -1
  33. package/packages/utils/src/server/correctOIDCUrl.test.ts +80 -19
  34. package/packages/utils/src/server/correctOIDCUrl.ts +89 -24
  35. package/packages/utils/src/server/index.ts +1 -0
  36. package/packages/utils/src/server/xor.test.ts +9 -7
  37. package/packages/utils/src/server/xor.ts +1 -1
  38. package/packages/web-crawler/package.json +1 -1
  39. package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +1 -1
  40. package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
  41. package/scripts/prebuild.mts +58 -1
  42. package/src/app/(backend)/api/auth/[...all]/route.ts +2 -1
  43. package/src/app/(backend)/middleware/auth/index.ts +3 -3
  44. package/src/app/(backend)/middleware/auth/utils.test.ts +1 -1
  45. package/src/app/(backend)/middleware/auth/utils.ts +1 -1
  46. package/src/app/(backend)/oidc/callback/desktop/route.ts +7 -36
  47. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +2 -2
  48. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +1 -1
  49. package/src/app/(backend)/webapi/plugin/gateway/route.ts +1 -1
  50. package/src/app/(backend)/webapi/proxy/route.ts +1 -1
  51. package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +1 -1
  52. package/src/app/[variants]/(auth)/reset-password/layout.tsx +1 -1
  53. package/src/app/[variants]/(auth)/signin/layout.tsx +1 -1
  54. package/src/app/[variants]/(auth)/signin/useSignIn.ts +2 -2
  55. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -1
  56. package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +12 -6
  57. package/src/app/[variants]/(auth)/verify-email/layout.tsx +1 -1
  58. package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -1
  59. package/src/app/[variants]/(main)/settings/security/index.tsx +1 -1
  60. package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +1 -1
  61. package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +1 -1
  62. package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -1
  63. package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +1 -1
  64. package/src/components/ModelSelect/index.tsx +103 -72
  65. package/src/envs/auth.ts +30 -9
  66. package/src/features/Conversation/Messages/AssistantGroup/components/EditState.tsx +15 -32
  67. package/src/features/Conversation/Messages/AssistantGroup/index.tsx +9 -7
  68. package/src/features/EditorModal/EditorCanvas.tsx +31 -50
  69. package/src/features/EditorModal/TextareCanvas.tsx +3 -1
  70. package/src/features/EditorModal/index.tsx +14 -4
  71. package/src/features/ModelSwitchPanel/components/Footer.tsx +42 -0
  72. package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +103 -0
  73. package/src/features/ModelSwitchPanel/components/List/SingleProviderModelItem.tsx +24 -0
  74. package/src/features/ModelSwitchPanel/components/List/VirtualItemRenderer.tsx +180 -0
  75. package/src/features/ModelSwitchPanel/components/List/index.tsx +99 -0
  76. package/src/features/ModelSwitchPanel/components/PanelContent.tsx +77 -0
  77. package/src/features/ModelSwitchPanel/components/Toolbar.tsx +54 -0
  78. package/src/features/ModelSwitchPanel/const.ts +29 -0
  79. package/src/features/ModelSwitchPanel/hooks/useBuildVirtualItems.ts +122 -0
  80. package/src/features/ModelSwitchPanel/hooks/useCurrentModelName.ts +18 -0
  81. package/src/features/ModelSwitchPanel/hooks/useDelayedRender.ts +18 -0
  82. package/src/features/ModelSwitchPanel/hooks/useModelAndProvider.ts +14 -0
  83. package/src/features/ModelSwitchPanel/hooks/usePanelHandlers.ts +33 -0
  84. package/src/features/ModelSwitchPanel/hooks/usePanelSize.ts +33 -0
  85. package/src/features/ModelSwitchPanel/hooks/usePanelState.ts +20 -0
  86. package/src/features/ModelSwitchPanel/index.tsx +25 -706
  87. package/src/features/ModelSwitchPanel/styles.ts +58 -0
  88. package/src/features/ModelSwitchPanel/types.ts +73 -0
  89. package/src/features/ModelSwitchPanel/utils.ts +24 -0
  90. package/src/features/User/UserPanel/PanelContent.tsx +1 -1
  91. package/src/features/User/__tests__/PanelContent.test.tsx +1 -1
  92. package/src/features/User/__tests__/UserAvatar.test.tsx +1 -1
  93. package/src/features/User/__tests__/useMenu.test.tsx +1 -1
  94. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -1
  95. package/src/libs/better-auth/auth-client.ts +7 -3
  96. package/src/libs/better-auth/define-config.ts +2 -2
  97. package/src/libs/next/proxy/define-config.ts +9 -6
  98. package/src/libs/oidc-provider/provider.test.ts +1 -1
  99. package/src/libs/trpc/async/context.ts +1 -1
  100. package/src/libs/trpc/lambda/context.ts +7 -8
  101. package/src/libs/trpc/middleware/userAuth.ts +1 -1
  102. package/src/libs/trusted-client/getSessionUser.ts +1 -1
  103. package/src/locales/default/components.ts +1 -0
  104. package/src/server/globalConfig/index.ts +2 -0
  105. package/src/server/routers/async/caller.ts +1 -1
  106. package/src/server/routers/lambda/__tests__/user.test.ts +2 -2
  107. package/src/server/routers/lambda/user.ts +2 -1
  108. package/src/services/_auth.ts +3 -3
  109. package/src/services/chat/index.ts +1 -1
  110. package/src/services/chat/mecha/contextEngineering.ts +1 -1
  111. package/src/store/global/initialState.ts +10 -0
  112. package/src/store/global/selectors/systemStatus.ts +5 -0
  113. package/src/store/serverConfig/selectors.ts +5 -1
  114. package/src/store/tool/slices/mcpStore/action.ts +74 -75
  115. package/src/store/user/slices/auth/action.test.ts +1 -1
  116. package/src/store/user/slices/auth/action.ts +1 -1
  117. package/src/store/user/slices/auth/initialState.ts +1 -1
  118. package/src/store/user/slices/auth/selectors.test.ts +1 -1
  119. package/src/store/user/slices/auth/selectors.ts +1 -1
  120. package/src/store/user/slices/common/action.ts +1 -1
  121. package/src/store/userMemory/slices/context/action.ts +6 -6
  122. package/packages/const/src/auth.ts +0 -14
  123. /package/e2e/src/features/{discover → community}/detail-pages.feature +0 -0
  124. /package/e2e/src/features/{discover → community}/interactions.feature +0 -0
  125. /package/e2e/src/features/{discover → community}/smoke.feature +0 -0
  126. /package/e2e/src/mocks/{discover → community}/data.ts +0 -0
  127. /package/e2e/src/mocks/{discover → community}/handlers.ts +0 -0
  128. /package/e2e/src/mocks/{discover → community}/index.ts +0 -0
  129. /package/e2e/src/mocks/{discover → community}/types.ts +0 -0
  130. /package/e2e/src/steps/{discover → community}/smoke.steps.ts +0 -0
@@ -5,29 +5,69 @@ import { validateRedirectHost } from './validateRedirectHost';
5
5
 
6
6
  const log = debug('lobe-oidc:correctOIDCUrl');
7
7
 
8
+ // Allowed protocols for security
9
+ const ALLOWED_PROTOCOLS = ['http', 'https'] as const;
10
+
8
11
  /**
9
12
  * Fix OIDC redirect URL issues in proxy environments
13
+ *
14
+ * This function:
15
+ * 1. Validates protocol against whitelist (http, https only)
16
+ * 2. Handles X-Forwarded-Host with multiple values (RFC 7239)
17
+ * 3. Validates X-Forwarded-Host against APP_URL to prevent open redirect attacks
18
+ * 4. Provides fallback logic for invalid forwarded values
19
+ *
20
+ * Note: Only X-Forwarded-Host is validated, not the Host header. This is because:
21
+ * - X-Forwarded-Host can be injected by attackers
22
+ * - Host header comes from the reverse proxy or direct access, which is trusted
23
+ *
10
24
  * @param req - Next.js request object
11
25
  * @param url - URL object to fix
12
26
  * @returns Fixed URL object
13
27
  */
14
28
  export const correctOIDCUrl = (req: NextRequest, url: URL): URL => {
29
+ log('Input URL: %s', url.toString());
30
+
31
+ // Get request headers for origin determination
15
32
  const requestHost = req.headers.get('host');
16
33
  const forwardedHost = req.headers.get('x-forwarded-host');
17
34
  const forwardedProto =
18
35
  req.headers.get('x-forwarded-proto') || req.headers.get('x-forwarded-protocol');
19
36
 
20
- log('Input URL: %s', url.toString());
21
37
  log(
22
- 'Request headers - host: %s, x-forwarded-host: %s, x-forwarded-proto: %s',
38
+ 'Getting safe origin - requestHost: %s, forwardedHost: %s, forwardedProto: %s',
23
39
  requestHost,
24
40
  forwardedHost,
25
41
  forwardedProto,
26
42
  );
27
43
 
28
- // Determine actual hostname and protocol with fallback values
29
- const actualHost = forwardedHost || requestHost;
30
- const actualProto = forwardedProto || (url.protocol === 'https:' ? 'https' : 'http');
44
+ // Determine actual hostname with fallback values
45
+ // Handle multiple hosts in X-Forwarded-Host (RFC 7239: comma-separated)
46
+ let actualHost = forwardedHost || requestHost;
47
+ if (forwardedHost && forwardedHost.includes(',')) {
48
+ // Take the first (leftmost) host as the original client's request
49
+ actualHost = forwardedHost.split(',')[0]!.trim();
50
+ log('Multiple hosts in X-Forwarded-Host, using first: %s', actualHost);
51
+ }
52
+
53
+ // Determine actual protocol with validation
54
+ // Use URL's protocol as fallback to preserve original behavior
55
+ let actualProto: string | null | undefined = forwardedProto;
56
+ if (actualProto) {
57
+ // Validate protocol is http or https
58
+ const protoLower = actualProto.toLowerCase();
59
+ if (!ALLOWED_PROTOCOLS.includes(protoLower as any)) {
60
+ log('Warning: Invalid protocol %s, ignoring', actualProto);
61
+ actualProto = null;
62
+ } else {
63
+ actualProto = protoLower;
64
+ }
65
+ }
66
+
67
+ // Fallback protocol priority: URL protocol > request.nextUrl.protocol > 'https'
68
+ if (!actualProto) {
69
+ actualProto = url.protocol === 'https:' ? 'https' : 'http';
70
+ }
31
71
 
32
72
  // If unable to determine valid hostname, return original URL
33
73
  if (!actualHost || actualHost === 'null') {
@@ -35,9 +75,30 @@ export const correctOIDCUrl = (req: NextRequest, url: URL): URL => {
35
75
  return url;
36
76
  }
37
77
 
38
- // Validate target host for security, prevent Open Redirect attacks
39
- if (!validateRedirectHost(actualHost)) {
40
- log('Warning: Target host %s failed validation, returning original URL', actualHost);
78
+ // Validate only X-Forwarded-Host for security, prevent Open Redirect attacks
79
+ // Host header is trusted (comes from reverse proxy or direct access)
80
+ if (forwardedHost && !validateRedirectHost(actualHost)) {
81
+ log('Warning: X-Forwarded-Host %s failed validation, falling back to request host', actualHost);
82
+ // Try to fall back to request host if forwarded host is invalid
83
+ if (requestHost) {
84
+ actualHost = requestHost;
85
+ } else {
86
+ // No valid host available
87
+ log('Error: No valid host available after validation, returning original URL');
88
+ return url;
89
+ }
90
+ }
91
+
92
+ // Build safe origin
93
+ const safeOrigin = `${actualProto}://${actualHost}`;
94
+ log('Safe origin: %s', safeOrigin);
95
+
96
+ // Parse safe origin to get hostname and protocol
97
+ let safeOriginUrl: URL;
98
+ try {
99
+ safeOriginUrl = new URL(safeOrigin);
100
+ } catch (error) {
101
+ log('Error parsing safe origin: %O', error);
41
102
  return url;
42
103
  }
43
104
 
@@ -46,24 +107,28 @@ export const correctOIDCUrl = (req: NextRequest, url: URL): URL => {
46
107
  url.hostname === 'localhost' ||
47
108
  url.hostname === '127.0.0.1' ||
48
109
  url.hostname === '0.0.0.0' ||
49
- url.hostname !== actualHost;
110
+ url.hostname !== safeOriginUrl.hostname;
111
+
112
+ if (!needsCorrection) {
113
+ log('URL does not need correction, returning original: %s', url.toString());
114
+ return url;
115
+ }
50
116
 
51
- if (needsCorrection) {
52
- log('URL needs correction. Original hostname: %s, correcting to: %s', url.hostname, actualHost);
117
+ log(
118
+ 'URL needs correction. Original hostname: %s, correcting to: %s',
119
+ url.hostname,
120
+ safeOriginUrl.hostname,
121
+ );
53
122
 
54
- try {
55
- const correctedUrl = new URL(url.toString());
56
- correctedUrl.protocol = actualProto + ':';
57
- correctedUrl.host = actualHost;
123
+ try {
124
+ const correctedUrl = new URL(url.toString());
125
+ correctedUrl.protocol = safeOriginUrl.protocol;
126
+ correctedUrl.host = safeOriginUrl.host;
58
127
 
59
- log('Corrected URL: %s', correctedUrl.toString());
60
- return correctedUrl;
61
- } catch (error) {
62
- log('Error creating corrected URL, returning original: %O', error);
63
- return url;
64
- }
128
+ log('Corrected URL: %s', correctedUrl.toString());
129
+ return correctedUrl;
130
+ } catch (error) {
131
+ log('Error creating corrected URL, returning original: %O', error);
132
+ return url;
65
133
  }
66
-
67
- log('URL does not need correction, returning original: %s', url.toString());
68
- return url;
69
134
  };
@@ -3,4 +3,5 @@ export * from './correctOIDCUrl';
3
3
  export * from './response';
4
4
  export * from './responsive';
5
5
  export * from './sse';
6
+ export * from './validateRedirectHost';
6
7
  export * from './xor';
@@ -1,5 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
+ import { SECRET_XOR_KEY } from '@/envs/auth';
4
+
3
5
  import { obfuscatePayloadWithXOR } from '../client/xor-obfuscation';
4
6
  import { getXorPayload } from './xor';
5
7
 
@@ -12,7 +14,7 @@ describe('getXorPayload', () => {
12
14
  };
13
15
 
14
16
  // 使用客户端的混淆函数生成token
15
- const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
17
+ const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
16
18
 
17
19
  // 使用服务端的解码函数解码
18
20
  const decodedPayload = getXorPayload(obfuscatedToken);
@@ -25,7 +27,7 @@ describe('getXorPayload', () => {
25
27
  userId: '12345',
26
28
  };
27
29
 
28
- const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
30
+ const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
29
31
  const decodedPayload = getXorPayload(obfuscatedToken);
30
32
 
31
33
  expect(decodedPayload).toEqual(originalPayload);
@@ -40,7 +42,7 @@ describe('getXorPayload', () => {
40
42
  awsSessionToken: 'session-token-example',
41
43
  };
42
44
 
43
- const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
45
+ const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
44
46
  const decodedPayload = getXorPayload(obfuscatedToken);
45
47
 
46
48
  expect(decodedPayload).toEqual(originalPayload);
@@ -54,7 +56,7 @@ describe('getXorPayload', () => {
54
56
  azureApiVersion: '2024-02-15-preview',
55
57
  };
56
58
 
57
- const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
59
+ const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
58
60
  const decodedPayload = getXorPayload(obfuscatedToken);
59
61
 
60
62
  expect(decodedPayload).toEqual(originalPayload);
@@ -67,7 +69,7 @@ describe('getXorPayload', () => {
67
69
  cloudflareBaseURLOrAccountID: 'account-id-example',
68
70
  };
69
71
 
70
- const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
72
+ const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
71
73
  const decodedPayload = getXorPayload(obfuscatedToken);
72
74
 
73
75
  expect(decodedPayload).toEqual(originalPayload);
@@ -76,7 +78,7 @@ describe('getXorPayload', () => {
76
78
  it('should handle empty payload correctly', () => {
77
79
  const originalPayload = {};
78
80
 
79
- const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
81
+ const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
80
82
  const decodedPayload = getXorPayload(obfuscatedToken);
81
83
 
82
84
  expect(decodedPayload).toEqual(originalPayload);
@@ -89,7 +91,7 @@ describe('getXorPayload', () => {
89
91
  apiKey: 'test-key',
90
92
  };
91
93
 
92
- const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload);
94
+ const obfuscatedToken = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
93
95
  const decodedPayload = getXorPayload(obfuscatedToken);
94
96
 
95
97
  expect(decodedPayload).toEqual(originalPayload);
@@ -1,6 +1,6 @@
1
1
  import { ClientSecretPayload } from '@lobechat/types';
2
2
 
3
- import { SECRET_XOR_KEY } from '@/const/auth';
3
+ import { SECRET_XOR_KEY } from '@/envs/auth';
4
4
 
5
5
  /**
6
6
  * Convert Base64 string to Uint8Array
@@ -13,7 +13,7 @@
13
13
  "happy-dom": "^20.0.11",
14
14
  "node-html-markdown": "^1.3.0",
15
15
  "query-string": "^9.3.1",
16
- "ssrf-safe-fetch": "workspace:*",
16
+ "@lobechat/ssrf-safe-fetch": "workspace:*",
17
17
  "url-join": "^5"
18
18
  }
19
19
  }
@@ -13,7 +13,7 @@ vi.mock('../../utils/withTimeout', () => ({
13
13
  withTimeout: vi.fn(),
14
14
  }));
15
15
 
16
- vi.mock('ssrf-safe-fetch', () => ({
16
+ vi.mock('@lobechat/ssrf-safe-fetch', () => ({
17
17
  ssrfSafeFetch: vi.fn(),
18
18
  }));
19
19
 
@@ -1,4 +1,4 @@
1
- import { ssrfSafeFetch } from 'ssrf-safe-fetch';
1
+ import { ssrfSafeFetch } from '@lobechat/ssrf-safe-fetch';
2
2
 
3
3
  import { CrawlImpl, CrawlSuccessResult } from '../type';
4
4
  import { NetworkConnectionError, PageNotFoundError, TimeoutError } from '../utils/errorType';
@@ -1,3 +1,4 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import * as dotenv from 'dotenv';
2
3
  import dotenvExpand from 'dotenv-expand';
3
4
  import { existsSync } from 'node:fs';
@@ -14,6 +15,61 @@ if (isDesktop) {
14
15
  } else {
15
16
  dotenvExpand.expand(dotenv.config());
16
17
  }
18
+
19
+ // Auth flags - use process.env directly for build-time dead code elimination
20
+ const enableClerk =
21
+ process.env.NEXT_PUBLIC_ENABLE_CLERK_AUTH === '1'
22
+ ? true
23
+ : !!process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
24
+ const enableBetterAuth = process.env.NEXT_PUBLIC_ENABLE_BETTER_AUTH === '1';
25
+ const enableNextAuth = process.env.NEXT_PUBLIC_ENABLE_NEXT_AUTH === '1';
26
+ const enableAuth = enableClerk || enableBetterAuth || enableNextAuth || false;
27
+
28
+ const getCommandVersion = (command: string): string | null => {
29
+ try {
30
+ return execSync(`${command} --version`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
31
+ .trim()
32
+ .split('\n')[0];
33
+ } catch {
34
+ return null;
35
+ }
36
+ };
37
+
38
+ const printEnvInfo = () => {
39
+ console.log('\n📋 Build Environment Info:');
40
+ console.log('─'.repeat(50));
41
+
42
+ // Runtime versions
43
+ console.log(` Node.js: ${process.version}`);
44
+ console.log(` npm: ${getCommandVersion('npm') ?? 'not installed'}`);
45
+
46
+ const bunVersion = getCommandVersion('bun');
47
+ if (bunVersion) console.log(` bun: ${bunVersion}`);
48
+
49
+ const pnpmVersion = getCommandVersion('pnpm');
50
+ if (pnpmVersion) console.log(` pnpm: ${pnpmVersion}`);
51
+
52
+ // Auth-related env vars
53
+ console.log('\n Auth Environment Variables:');
54
+ console.log(` NEXT_PUBLIC_AUTH_URL: ${process.env.NEXT_PUBLIC_AUTH_URL ?? '(not set)'}`);
55
+ console.log(` NEXTAUTH_URL: ${process.env.NEXTAUTH_URL ?? '(not set)'}`);
56
+ console.log(` APP_URL: ${process.env.APP_URL ?? '(not set)'}`);
57
+ console.log(` VERCEL_URL: ${process.env.VERCEL_URL ?? '(not set)'}`);
58
+ console.log(` AUTH_EMAIL_VERIFICATION: ${process.env.AUTH_EMAIL_VERIFICATION ?? '(not set)'}`);
59
+ console.log(` ENABLE_MAGIC_LINK: ${process.env.ENABLE_MAGIC_LINK ?? '(not set)'}`);
60
+ console.log(` AUTH_SECRET: ${process.env.AUTH_SECRET ? '✓ set' : '✗ not set'}`);
61
+ console.log(` KEY_VAULTS_SECRET: ${process.env.KEY_VAULTS_SECRET ? '✓ set' : '✗ not set'}`);
62
+
63
+ // Auth flags
64
+ console.log('\n Auth Flags:');
65
+ console.log(` enableClerk: ${enableClerk}`);
66
+ console.log(` enableBetterAuth: ${enableBetterAuth}`);
67
+ console.log(` enableNextAuth: ${enableNextAuth}`);
68
+ console.log(` enableAuth: ${enableAuth}`);
69
+
70
+ console.log('─'.repeat(50));
71
+ };
72
+
17
73
  // 创建需要排除的特性映射
18
74
  /* eslint-disable sort-keys-fix/sort-keys-fix */
19
75
  const partialBuildPages = [
@@ -105,8 +161,9 @@ export const runPrebuild = async (targetDir: string = 'src') => {
105
161
  const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
106
162
 
107
163
  if (isMainModule) {
164
+ printEnvInfo();
108
165
  // 执行删除操作
109
- console.log('Starting prebuild cleanup...');
166
+ console.log('\nStarting prebuild cleanup...');
110
167
  await runPrebuild();
111
168
  console.log('Prebuild cleanup completed.');
112
169
  }
@@ -1,6 +1,7 @@
1
- import { enableBetterAuth, enableNextAuth } from '@lobechat/const';
2
1
  import type { NextRequest } from 'next/server';
3
2
 
3
+ import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
4
+
4
5
  const createHandler = async () => {
5
6
  if (enableBetterAuth) {
6
7
  const [{ toNextJsHandler }, { auth }] = await Promise.all([
@@ -8,15 +8,15 @@ import { ChatErrorType, type ClientSecretPayload } from '@lobechat/types';
8
8
  import { getXorPayload } from '@lobechat/utils/server';
9
9
  import { type NextRequest } from 'next/server';
10
10
 
11
+ import { getServerDB } from '@/database/core/db-adaptor';
12
+ import { type LobeChatDatabase } from '@/database/type';
11
13
  import {
12
14
  LOBE_CHAT_AUTH_HEADER,
13
15
  LOBE_CHAT_OIDC_AUTH_HEADER,
14
16
  OAUTH_AUTHORIZED,
15
17
  enableBetterAuth,
16
18
  enableClerk,
17
- } from '@/const/auth';
18
- import { getServerDB } from '@/database/core/db-adaptor';
19
- import { type LobeChatDatabase } from '@/database/type';
19
+ } from '@/envs/auth';
20
20
  import { ClerkAuth } from '@/libs/clerk-auth';
21
21
  import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
22
22
  import { createErrorResponse } from '@/utils/errorResponse';
@@ -7,7 +7,7 @@ let enableClerkMock = false;
7
7
  let enableNextAuthMock = false;
8
8
  let enableBetterAuthMock = false;
9
9
 
10
- vi.mock('@/const/auth', async (importOriginal) => {
10
+ vi.mock('@/envs/auth', async (importOriginal) => {
11
11
  const data = await importOriginal();
12
12
 
13
13
  return {
@@ -2,7 +2,7 @@ import { type AuthObject } from '@clerk/backend';
2
2
  import { AgentRuntimeError } from '@lobechat/model-runtime';
3
3
  import { ChatErrorType } from '@lobechat/types';
4
4
 
5
- import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth';
5
+ import { enableBetterAuth, enableClerk, enableNextAuth } from '@/envs/auth';
6
6
 
7
7
  interface CheckAuthParams {
8
8
  apiKey?: string;
@@ -10,41 +10,15 @@ const log = debug('lobe-oidc:callback:desktop');
10
10
  const errorPathname = '/oauth/callback/error';
11
11
 
12
12
  /**
13
- * 安全地构建重定向URL
13
+ * 安全地构建重定向URL,使用经过验证的 correctOIDCUrl 防止开放重定向攻击
14
14
  */
15
15
  const buildRedirectUrl = (req: NextRequest, pathname: string): URL => {
16
- const forwardedHost = req.headers.get('x-forwarded-host');
17
- const requestHost = req.headers.get('host');
18
- const forwardedProto =
19
- req.headers.get('x-forwarded-proto') || req.headers.get('x-forwarded-protocol');
20
-
21
- // 确定实际的主机名,提供后备值
22
- const actualHost = forwardedHost || requestHost;
23
- const actualProto = forwardedProto || 'https';
24
-
25
- log(
26
- 'Building redirect URL - host: %s, proto: %s, pathname: %s',
27
- actualHost,
28
- actualProto,
29
- pathname,
30
- );
31
-
32
- // 如果主机名仍然无效,使用req.nextUrl作为后备
33
- if (!actualHost) {
34
- log('Warning: Invalid host detected, using req.nextUrl as fallback');
35
- const fallbackUrl = req.nextUrl.clone();
36
- fallbackUrl.pathname = pathname;
37
- return fallbackUrl;
38
- }
16
+ // 使用 req.nextUrl 作为基础URL,然后通过 correctOIDCUrl 进行验证和修正
17
+ const baseUrl = req.nextUrl.clone();
18
+ baseUrl.pathname = pathname;
39
19
 
40
- try {
41
- return new URL(`${actualProto}://${actualHost}${pathname}`);
42
- } catch (error) {
43
- log('Error constructing URL, using req.nextUrl as fallback: %O', error);
44
- const fallbackUrl = req.nextUrl.clone();
45
- fallbackUrl.pathname = pathname;
46
- return fallbackUrl;
47
- }
20
+ // correctOIDCUrl 会验证 X-Forwarded-* 头部并防止开放重定向攻击
21
+ return correctOIDCUrl(req, baseUrl);
48
22
  };
49
23
 
50
24
  export const GET = async (req: NextRequest) => {
@@ -82,9 +56,6 @@ export const GET = async (req: NextRequest) => {
82
56
  log('Request x-forwarded-proto: %s', req.headers.get('x-forwarded-proto'));
83
57
  log('Constructed success URL: %s', successUrl.toString());
84
58
 
85
- const correctedUrl = correctOIDCUrl(req, successUrl);
86
- log('Final redirect URL: %s', correctedUrl.toString());
87
-
88
59
  // cleanup expired
89
60
  after(async () => {
90
61
  const cleanedCount = await authHandoffModel.cleanupExpired();
@@ -92,7 +63,7 @@ export const GET = async (req: NextRequest) => {
92
63
  log('Cleaned up %d expired handoff records', cleanedCount);
93
64
  });
94
65
 
95
- return NextResponse.redirect(correctedUrl);
66
+ return NextResponse.redirect(successUrl);
96
67
  } catch (error) {
97
68
  log('Error in OIDC callback: %O', error);
98
69
 
@@ -6,7 +6,7 @@ import { getXorPayload } from '@lobechat/utils/server';
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
7
 
8
8
  import { checkAuthMethod } from '@/app/(backend)/middleware/auth/utils';
9
- import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/const/auth';
9
+ import { LOBE_CHAT_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/envs/auth';
10
10
  import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
11
11
 
12
12
  import { POST } from './route';
@@ -32,7 +32,7 @@ vi.mock('@/server/modules/ModelRuntime', () => ({
32
32
  const mockState = vi.hoisted(() => ({ enableClerk: false }));
33
33
 
34
34
  // 模拟 @/const/auth 模块
35
- vi.mock('@/const/auth', async (importOriginal) => {
35
+ vi.mock('@/envs/auth', async (importOriginal) => {
36
36
  const modules = await importOriginal();
37
37
  return {
38
38
  ...(modules as any),
@@ -4,7 +4,7 @@ import { ChatErrorType } from '@lobechat/types';
4
4
  import { getXorPayload } from '@lobechat/utils/server';
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
6
 
7
- import { LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
7
+ import { LOBE_CHAT_AUTH_HEADER } from '@/envs/auth';
8
8
  import { initModelRuntimeFromDB } from '@/server/modules/ModelRuntime';
9
9
 
10
10
  import { GET } from './route';
@@ -3,9 +3,9 @@ import { ChatErrorType, TraceNameMap } from '@lobechat/types';
3
3
  import type { PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
4
4
  import { createGatewayOnEdgeRuntime } from '@lobehub/chat-plugins-gateway';
5
5
 
6
- import { LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
7
6
  import { LOBE_CHAT_TRACE_ID } from '@/const/trace';
8
7
  import { getAppConfig } from '@/envs/app';
8
+ import { LOBE_CHAT_AUTH_HEADER } from '@/envs/auth';
9
9
  import { TraceClient } from '@/libs/traces';
10
10
  import { parserPluginSettings } from '@/server/services/pluginGateway/settings';
11
11
  import { getTracePayload } from '@/utils/trace';
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { ssrfSafeFetch } from 'ssrf-safe-fetch';
2
+ import { ssrfSafeFetch } from '@lobechat/ssrf-safe-fetch';
3
3
 
4
4
  /**
5
5
  * just for a proxy
@@ -2,7 +2,7 @@ import { SignIn } from '@clerk/nextjs';
2
2
  import { BRANDING_NAME } from '@lobechat/business-const';
3
3
  import { notFound } from 'next/navigation';
4
4
 
5
- import { enableClerk } from '@/const/auth';
5
+ import { enableClerk } from '@/envs/auth';
6
6
  import { metadataModule } from '@/server/metadata';
7
7
  import { translation } from '@/server/translation';
8
8
  import { type DynamicLayoutProps } from '@/types/next';
@@ -1,7 +1,7 @@
1
1
  import { notFound } from 'next/navigation';
2
2
  import { type PropsWithChildren } from 'react';
3
3
 
4
- import { enableBetterAuth } from '@/const/auth';
4
+ import { enableBetterAuth } from '@/envs/auth';
5
5
 
6
6
  const Layout = ({ children }: PropsWithChildren) => {
7
7
  if (!enableBetterAuth) return notFound();
@@ -1,7 +1,7 @@
1
1
  import { notFound } from 'next/navigation';
2
2
  import { type PropsWithChildren } from 'react';
3
3
 
4
- import { enableBetterAuth } from '@/const/auth';
4
+ import { enableBetterAuth } from '@/envs/auth';
5
5
 
6
6
  const Layout = ({ children }: PropsWithChildren) => {
7
7
  if (!enableBetterAuth) return notFound();
@@ -8,10 +8,10 @@ import type { CheckUserResponseData } from '@/app/(backend)/api/auth/check-user/
8
8
  import type { ResolveUsernameResponseData } from '@/app/(backend)/api/auth/resolve-username/route';
9
9
  import { useBusinessSignin } from '@/business/client/hooks/useBusinessSignin';
10
10
  import { message } from '@/components/AntdStaticMethods';
11
- import { getAuthConfig } from '@/envs/auth';
12
11
  import { requestPasswordReset, signIn } from '@/libs/better-auth/auth-client';
13
12
  import { isBuiltinProvider, normalizeProviderId } from '@/libs/better-auth/utils/client';
14
13
  import { useServerConfigStore } from '@/store/serverConfig';
14
+ import { serverConfigSelectors } from '@/store/serverConfig/selectors';
15
15
 
16
16
  import { EMAIL_REGEX, USERNAME_REGEX } from './SignInEmailStep';
17
17
 
@@ -31,7 +31,7 @@ export const useSignIn = () => {
31
31
  const { t } = useTranslation('auth');
32
32
  const router = useRouter();
33
33
  const searchParams = useSearchParams();
34
- const { NEXT_PUBLIC_ENABLE_MAGIC_LINK: enableMagicLink } = getAuthConfig();
34
+ const enableMagicLink = useServerConfigStore(serverConfigSelectors.enableMagicLink);
35
35
  const [form] = Form.useForm<SignInFormValues>();
36
36
  const [loading, setLoading] = useState(false);
37
37
  const [socialLoading, setSocialLoading] = useState<string | null>(null);
@@ -1,7 +1,7 @@
1
1
  import { SignUp } from '@clerk/nextjs';
2
2
  import { notFound } from 'next/navigation';
3
3
 
4
- import { enableBetterAuth, enableClerk } from '@/const/auth';
4
+ import { enableBetterAuth, enableClerk } from '@/envs/auth';
5
5
  import { metadataModule } from '@/server/metadata';
6
6
  import { translation } from '@/server/translation';
7
7
  import { type DynamicLayoutProps } from '@/types/next';
@@ -2,24 +2,30 @@ import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
2
2
  import { form } from 'motion/react-m';
3
3
  import { useRouter, useSearchParams } from 'next/navigation';
4
4
  import { useState } from 'react';
5
+ import { useTranslation } from 'react-i18next';
5
6
 
6
7
  import {
7
8
  BusinessSignupFomData,
8
9
  useBusinessSignup,
9
10
  } from '@/business/client/hooks/useBusinessSignup';
10
11
  import { message } from '@/components/AntdStaticMethods';
11
- import { authEnv } from '@/envs/auth';
12
12
  import { signUp } from '@/libs/better-auth/auth-client';
13
+ import { useServerConfigStore } from '@/store/serverConfig';
14
+ import { serverConfigSelectors } from '@/store/serverConfig/selectors';
13
15
 
14
16
  import { BaseSignUpFormValues } from './types';
15
17
 
16
18
  export type SignUpFormValues = BaseSignUpFormValues & BusinessSignupFomData;
17
19
 
18
20
  export const useSignUp = () => {
21
+ const { t } = useTranslation('auth');
19
22
  const router = useRouter();
20
23
  const searchParams = useSearchParams();
21
24
  const [loading, setLoading] = useState(false);
22
25
  const { getFetchOptions, preSocialSignupCheck, businessElement } = useBusinessSignup(form);
26
+ const enableEmailVerification = useServerConfigStore(
27
+ serverConfigSelectors.enableEmailVerification,
28
+ );
23
29
 
24
30
  const handleSignUp = async (values: SignUpFormValues) => {
25
31
  setLoading(true);
@@ -46,20 +52,20 @@ export const useSignUp = () => {
46
52
  (error as any)?.details?.cause?.code === '23505';
47
53
 
48
54
  if (isEmailDuplicate) {
49
- message.error('betterAuth.errors.emailExists');
55
+ message.error(t('betterAuth.errors.emailExists'));
50
56
  return;
51
57
  }
52
58
 
53
59
  if (error.code === 'INVALID_EMAIL') {
54
- message.error('betterAuth.errors.emailInvalid');
60
+ message.error(t('betterAuth.errors.emailInvalid'));
55
61
  return;
56
62
  }
57
63
 
58
- message.error(error.message || 'betterAuth.signup.error');
64
+ message.error(error.message || t('betterAuth.signup.error'));
59
65
  return;
60
66
  }
61
67
 
62
- if (authEnv.NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION) {
68
+ if (enableEmailVerification) {
63
69
  router.push(
64
70
  `/verify-email?email=${encodeURIComponent(values.email)}&callbackUrl=${encodeURIComponent(callbackUrl)}`,
65
71
  );
@@ -67,7 +73,7 @@ export const useSignUp = () => {
67
73
  router.push(callbackUrl);
68
74
  }
69
75
  } catch {
70
- message.error('betterAuth.signup.error');
76
+ message.error(t('betterAuth.signup.error'));
71
77
  } finally {
72
78
  setLoading(false);
73
79
  }
@@ -1,7 +1,7 @@
1
1
  import { notFound } from 'next/navigation';
2
2
  import { type PropsWithChildren } from 'react';
3
3
 
4
- import { enableBetterAuth } from '@/const/auth';
4
+ import { enableBetterAuth } from '@/envs/auth';
5
5
 
6
6
  const Layout = ({ children }: PropsWithChildren) => {
7
7
  if (!enableBetterAuth) return notFound();