@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
@@ -1,7 +1,11 @@
1
+ import { resolve } from 'node:path';
1
2
  import { defineConfig } from 'vitest/config';
2
3
 
3
4
  export default defineConfig({
4
5
  test: {
6
+ alias: {
7
+ '@': resolve(__dirname, '../../src'),
8
+ },
5
9
  coverage: {
6
10
  reporter: ['text', 'json', 'lcov', 'text-summary'],
7
11
  },
@@ -160,7 +160,7 @@ export class LobeReplicateAI implements LobeRuntimeAI {
160
160
  '[Replicate createImage] Local URL detected, will fetch and upload as data',
161
161
  );
162
162
  try {
163
- const { ssrfSafeFetch } = await import('ssrf-safe-fetch');
163
+ const { ssrfSafeFetch } = await import('@lobechat/ssrf-safe-fetch');
164
164
  const imageResponse = await ssrfSafeFetch(imageUrl);
165
165
  if (!imageResponse.ok) {
166
166
  throw new Error(
@@ -117,7 +117,7 @@ describe('ssrfSafeFetch', () => {
117
117
  });
118
118
 
119
119
  it('should allow private IPs when SSRF_ALLOW_PRIVATE_IP_ADDRESS is true', async () => {
120
- process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS = 'true';
120
+ process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS = '1';
121
121
 
122
122
  const mockResponse = createMockResponse();
123
123
  mockFetch.mockResolvedValue(mockResponse);
@@ -277,7 +277,7 @@ describe('ssrfSafeFetch', () => {
277
277
  describe('integration scenarios', () => {
278
278
  it('should work with complex request configurations', async () => {
279
279
  process.env.SSRF_ALLOW_IP_ADDRESS_LIST = '127.0.0.1';
280
- process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS = 'true';
280
+ process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS = '1';
281
281
 
282
282
  const mockResponse = createMockResponse({
283
283
  // @ts-ignore
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "ssrf-safe-fetch",
2
+ "name": "@lobechat/ssrf-safe-fetch",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "description": "SSRF-safe fetch implementation with browser/node conditional exports",
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "main": "index.ts",
14
14
  "scripts": {
15
- "test": "vitest run"
15
+ "test": "vitest run",
16
+ "test:coverage": "vitest --coverage --silent='passed-only'"
16
17
  },
17
18
  "dependencies": {
18
19
  "node-fetch": "^3.3.2",
@@ -49,7 +49,9 @@ export type ServerLanguageModel = Partial<Record<GlobalLLMProviderKey, ServerMod
49
49
  export interface GlobalServerConfig {
50
50
  aiProvider: ServerLanguageModel;
51
51
  defaultAgent?: PartialDeep<UserDefaultAgent>;
52
+ enableEmailVerification?: boolean;
52
53
  enableKlavis?: boolean;
54
+ enableMagicLink?: boolean;
53
55
  enableMarketTrustedClient?: boolean;
54
56
  enableUploadFileToServer?: boolean;
55
57
  enabledAccessCode?: boolean;
@@ -34,7 +34,7 @@
34
34
  "remark": "^15.0.1",
35
35
  "remark-gfm": "^4.0.1",
36
36
  "remark-html": "^16.0.1",
37
- "ssrf-safe-fetch": "workspace:*",
37
+ "@lobechat/ssrf-safe-fetch": "workspace:*",
38
38
  "tokenx": "^1.2.1",
39
39
  "ua-parser-js": "^1.0.41",
40
40
  "uuid": "^11.1.0",
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { SECRET_XOR_KEY } from '@/const/auth';
3
+ import { SECRET_XOR_KEY } from '@/envs/auth';
4
4
 
5
5
  import { obfuscatePayloadWithXOR } from './xor-obfuscation';
6
6
 
@@ -8,7 +8,7 @@ describe('xor-obfuscation', () => {
8
8
  describe('obfuscatePayloadWithXOR', () => {
9
9
  it('应该对简单字符串进行混淆并返回Base64字符串', () => {
10
10
  const payload = 'hello world';
11
- const result = obfuscatePayloadWithXOR(payload);
11
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
12
12
 
13
13
  // 验证返回值是字符串
14
14
  expect(typeof result).toBe('string');
@@ -22,7 +22,7 @@ describe('xor-obfuscation', () => {
22
22
 
23
23
  it('应该对JSON对象进行混淆', () => {
24
24
  const payload = { name: 'test', value: 123, active: true };
25
- const result = obfuscatePayloadWithXOR(payload);
25
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
26
26
 
27
27
  // 验证返回值是字符串
28
28
  expect(typeof result).toBe('string');
@@ -33,7 +33,7 @@ describe('xor-obfuscation', () => {
33
33
 
34
34
  it('应该对数组进行混淆', () => {
35
35
  const payload = [1, 2, 3, 'test', { nested: true }];
36
- const result = obfuscatePayloadWithXOR(payload);
36
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
37
37
 
38
38
  // 验证返回值是字符串
39
39
  expect(typeof result).toBe('string');
@@ -58,7 +58,7 @@ describe('xor-obfuscation', () => {
58
58
  tokens: ['abc123', 'def456'],
59
59
  metadata: null,
60
60
  };
61
- const result = obfuscatePayloadWithXOR(payload);
61
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
62
62
 
63
63
  // 验证返回值是字符串
64
64
  expect(typeof result).toBe('string');
@@ -69,8 +69,8 @@ describe('xor-obfuscation', () => {
69
69
 
70
70
  it('相同的输入应该产生相同的输出', () => {
71
71
  const payload = { test: 'consistent' };
72
- const result1 = obfuscatePayloadWithXOR(payload);
73
- const result2 = obfuscatePayloadWithXOR(payload);
72
+ const result1 = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
73
+ const result2 = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
74
74
 
75
75
  expect(result1).toBe(result2);
76
76
  });
@@ -79,15 +79,15 @@ describe('xor-obfuscation', () => {
79
79
  const payload1 = { test: 'value1' };
80
80
  const payload2 = { test: 'value2' };
81
81
 
82
- const result1 = obfuscatePayloadWithXOR(payload1);
83
- const result2 = obfuscatePayloadWithXOR(payload2);
82
+ const result1 = obfuscatePayloadWithXOR(payload1, SECRET_XOR_KEY);
83
+ const result2 = obfuscatePayloadWithXOR(payload2, SECRET_XOR_KEY);
84
84
 
85
85
  expect(result1).not.toBe(result2);
86
86
  });
87
87
 
88
88
  it('应该处理包含特殊字符的字符串', () => {
89
89
  const payload = 'Hello! @#$%^&*()_+-=[]{}|;:,.<>?/~`"\'\\';
90
- const result = obfuscatePayloadWithXOR(payload);
90
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
91
91
 
92
92
  // 验证返回值是字符串
93
93
  expect(typeof result).toBe('string');
@@ -98,7 +98,7 @@ describe('xor-obfuscation', () => {
98
98
 
99
99
  it('应该处理包含Unicode字符的字符串', () => {
100
100
  const payload = '你好世界 🌍 émojis 日本語 한국어';
101
- const result = obfuscatePayloadWithXOR(payload);
101
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
102
102
 
103
103
  // 验证返回值是字符串
104
104
  expect(typeof result).toBe('string');
@@ -109,7 +109,7 @@ describe('xor-obfuscation', () => {
109
109
 
110
110
  it('应该处理空字符串', () => {
111
111
  const payload = '';
112
- const result = obfuscatePayloadWithXOR(payload);
112
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
113
113
 
114
114
  // 验证返回值是字符串
115
115
  expect(typeof result).toBe('string');
@@ -120,7 +120,7 @@ describe('xor-obfuscation', () => {
120
120
 
121
121
  it('应该处理空对象', () => {
122
122
  const payload = {};
123
- const result = obfuscatePayloadWithXOR(payload);
123
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
124
124
 
125
125
  // 验证返回值是字符串
126
126
  expect(typeof result).toBe('string');
@@ -130,7 +130,7 @@ describe('xor-obfuscation', () => {
130
130
  });
131
131
 
132
132
  it('应该处理空数组', () => {
133
- const result = obfuscatePayloadWithXOR([]);
133
+ const result = obfuscatePayloadWithXOR([], SECRET_XOR_KEY);
134
134
 
135
135
  // 验证返回值是字符串
136
136
  expect(typeof result).toBe('string');
@@ -141,7 +141,7 @@ describe('xor-obfuscation', () => {
141
141
 
142
142
  it('应该处理null值', () => {
143
143
  const payload = null;
144
- const result = obfuscatePayloadWithXOR(payload);
144
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
145
145
 
146
146
  // 验证返回值是字符串
147
147
  expect(typeof result).toBe('string');
@@ -152,7 +152,7 @@ describe('xor-obfuscation', () => {
152
152
 
153
153
  it('应该处理数字', () => {
154
154
  const payload = 42;
155
- const result = obfuscatePayloadWithXOR(payload);
155
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
156
156
 
157
157
  // 验证返回值是字符串
158
158
  expect(typeof result).toBe('string');
@@ -165,8 +165,8 @@ describe('xor-obfuscation', () => {
165
165
  const payloadTrue = true;
166
166
  const payloadFalse = false;
167
167
 
168
- const resultTrue = obfuscatePayloadWithXOR(payloadTrue);
169
- const resultFalse = obfuscatePayloadWithXOR(payloadFalse);
168
+ const resultTrue = obfuscatePayloadWithXOR(payloadTrue, SECRET_XOR_KEY);
169
+ const resultFalse = obfuscatePayloadWithXOR(payloadFalse, SECRET_XOR_KEY);
170
170
 
171
171
  // 验证返回值是字符串
172
172
  expect(typeof resultTrue).toBe('string');
@@ -189,7 +189,7 @@ describe('xor-obfuscation', () => {
189
189
  tab: 'col1\tcol2',
190
190
  unicode: '\u0041\u0042\u0043',
191
191
  };
192
- const result = obfuscatePayloadWithXOR(payload);
192
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
193
193
 
194
194
  // 验证返回值是字符串
195
195
  expect(typeof result).toBe('string');
@@ -200,7 +200,7 @@ describe('xor-obfuscation', () => {
200
200
 
201
201
  it('应该处理很长的字符串', () => {
202
202
  const payload = 'a'.repeat(10000);
203
- const result = obfuscatePayloadWithXOR(payload);
203
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
204
204
 
205
205
  // 验证返回值是字符串
206
206
  expect(typeof result).toBe('string');
@@ -216,8 +216,8 @@ describe('xor-obfuscation', () => {
216
216
  const shortPayload = 'short';
217
217
  const longPayload = 'this is a much longer string that should produce different output';
218
218
 
219
- const shortResult = obfuscatePayloadWithXOR(shortPayload);
220
- const longResult = obfuscatePayloadWithXOR(longPayload);
219
+ const shortResult = obfuscatePayloadWithXOR(shortPayload, SECRET_XOR_KEY);
220
+ const longResult = obfuscatePayloadWithXOR(longPayload, SECRET_XOR_KEY);
221
221
 
222
222
  // 较长的输入应该产生较长的输出
223
223
  expect(longResult.length).toBeGreaterThan(shortResult.length);
@@ -225,7 +225,7 @@ describe('xor-obfuscation', () => {
225
225
 
226
226
  it('应该验证输出是有效的Base64格式', () => {
227
227
  const payload = { test: 'base64 validation' };
228
- const result = obfuscatePayloadWithXOR(payload);
228
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
229
229
 
230
230
  // 验证Base64格式的正则表达式
231
231
  const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
@@ -242,14 +242,14 @@ describe('xor-obfuscation', () => {
242
242
  },
243
243
  };
244
244
 
245
- const result = obfuscatePayloadWithXOR(payload);
245
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
246
246
  expect(typeof result).toBe('string');
247
247
  expect(() => atob(result)).not.toThrow();
248
248
  });
249
249
 
250
250
  it('应该对undefined值进行处理', () => {
251
251
  const payload = undefined;
252
- const result = obfuscatePayloadWithXOR(payload);
252
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
253
253
 
254
254
  // 验证返回值是字符串
255
255
  expect(typeof result).toBe('string');
@@ -268,7 +268,7 @@ describe('xor-obfuscation', () => {
268
268
  value: 123,
269
269
  };
270
270
 
271
- const result = obfuscatePayloadWithXOR(payload);
271
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
272
272
  expect(typeof result).toBe('string');
273
273
  expect(() => atob(result)).not.toThrow();
274
274
  });
@@ -279,7 +279,7 @@ describe('xor-obfuscation', () => {
279
279
 
280
280
  // 多次运行相同输入
281
281
  for (let i = 0; i < 10; i++) {
282
- results.push(obfuscatePayloadWithXOR(payload));
282
+ results.push(obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY));
283
283
  }
284
284
 
285
285
  // 所有结果应该相同
@@ -293,7 +293,7 @@ describe('xor-obfuscation', () => {
293
293
  name: 'date test',
294
294
  };
295
295
 
296
- const result = obfuscatePayloadWithXOR(payload);
296
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
297
297
  expect(typeof result).toBe('string');
298
298
  expect(() => atob(result)).not.toThrow();
299
299
  });
@@ -306,7 +306,7 @@ describe('xor-obfuscation', () => {
306
306
  normalKey: 'normal value',
307
307
  };
308
308
 
309
- const result = obfuscatePayloadWithXOR(payload);
309
+ const result = obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY);
310
310
  expect(typeof result).toBe('string');
311
311
  expect(() => atob(result)).not.toThrow();
312
312
  });
@@ -314,7 +314,7 @@ describe('xor-obfuscation', () => {
314
314
  it('应该验证混淆后的数据长度合理性', () => {
315
315
  const originalPayload = { test: 'length check' };
316
316
  const originalJSON = JSON.stringify(originalPayload);
317
- const result = obfuscatePayloadWithXOR(originalPayload);
317
+ const result = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
318
318
 
319
319
  // Base64 编码后的长度通常是原始长度的 4/3 倍(向上取整到4的倍数)
320
320
  const expectedMinLength = Math.ceil((originalJSON.length * 4) / 3 / 4) * 4;
@@ -323,7 +323,7 @@ describe('xor-obfuscation', () => {
323
323
 
324
324
  it('应该验证XOR操作的正确性(通过逆向操作)', () => {
325
325
  const originalPayload = { message: 'XOR test', value: 42 };
326
- const obfuscatedResult = obfuscatePayloadWithXOR(originalPayload);
326
+ const obfuscatedResult = obfuscatePayloadWithXOR(originalPayload, SECRET_XOR_KEY);
327
327
 
328
328
  // 手动实现逆向操作来验证 XOR 操作的正确性
329
329
  const base64Decoded = atob(obfuscatedResult);
@@ -357,7 +357,7 @@ describe('xor-obfuscation', () => {
357
357
  [4, 5, 6],
358
358
  ];
359
359
 
360
- const results = payloads.map((payload) => obfuscatePayloadWithXOR(payload));
360
+ const results = payloads.map((payload) => obfuscatePayloadWithXOR(payload, SECRET_XOR_KEY));
361
361
 
362
362
  // 验证所有结果都不相同
363
363
  for (let i = 0; i < results.length; i++) {
@@ -1,5 +1,3 @@
1
- import { SECRET_XOR_KEY } from '@/const/auth';
2
-
3
1
  /**
4
2
  * Convert string to Uint8Array (UTF-8 encoding)
5
3
  */
@@ -24,12 +22,13 @@ const xorProcess = (data: Uint8Array, key: Uint8Array): Uint8Array => {
24
22
  /**
25
23
  * Obfuscate payload with XOR and encode to Base64
26
24
  * @param payload The JSON object to obfuscate
25
+ * @param secretKey The key used for XOR obfuscation
27
26
  * @returns The obfuscated string encoded in Base64
28
27
  */
29
- export const obfuscatePayloadWithXOR = <T>(payload: T): string => {
28
+ export const obfuscatePayloadWithXOR = <T>(payload: T, secretKey: string): string => {
30
29
  const jsonString = JSON.stringify(payload);
31
30
  const dataBytes = stringToUint8Array(jsonString);
32
- const keyBytes = stringToUint8Array(SECRET_XOR_KEY);
31
+ const keyBytes = stringToUint8Array(secretKey);
33
32
 
34
33
  const xoredBytes = xorProcess(dataBytes, keyBytes);
35
34
 
@@ -48,7 +48,7 @@ export const imageUrlToBase64 = async (
48
48
 
49
49
  // Use SSRF-safe fetch on server-side to prevent SSRF attacks
50
50
  const res = isServer
51
- ? await import('ssrf-safe-fetch').then((m) => m.ssrfSafeFetch(imageUrl))
51
+ ? await import('@lobechat/ssrf-safe-fetch').then((m) => m.ssrfSafeFetch(imageUrl))
52
52
  : await fetch(imageUrl);
53
53
 
54
54
  const blob = await res.blob();
@@ -7,7 +7,7 @@ let mockEnableBetterAuth = false;
7
7
  let mockEnableClerk = false;
8
8
  let mockEnableNextAuth = false;
9
9
 
10
- vi.mock('@/const/auth', () => ({
10
+ vi.mock('@/envs/auth', () => ({
11
11
  get enableBetterAuth() {
12
12
  return mockEnableBetterAuth;
13
13
  },
@@ -1,6 +1,6 @@
1
1
  import { headers } from 'next/headers';
2
2
 
3
- import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth';
3
+ import { enableBetterAuth, enableClerk, enableNextAuth } from '@/envs/auth';
4
4
 
5
5
  export const getUserAuth = async () => {
6
6
  if (enableClerk) {
@@ -338,9 +338,8 @@ describe('correctOIDCUrl', () => {
338
338
  const originalUrl = new URL('http://localhost:3000/auth/callback');
339
339
  const result = correctOIDCUrl(mockRequest, originalUrl);
340
340
 
341
- // Should return original URL because example.com:8443 doesn't match configured APP_URL (https://example.com)
342
- expect(result).toBe(originalUrl);
343
- expect(result.toString()).toBe('http://localhost:3000/auth/callback');
341
+ // Should fall back to host header because example.com:8443 doesn't match configured APP_URL
342
+ expect(result.toString()).toBe('https://internal.com:3000/auth/callback');
344
343
  });
345
344
 
346
345
  it('should not need correction when URL hostname matches actual host', () => {
@@ -358,18 +357,18 @@ describe('correctOIDCUrl', () => {
358
357
  });
359
358
 
360
359
  describe('Open Redirect protection', () => {
361
- it('should prevent redirection to malicious external domains', () => {
360
+ it('should prevent redirection to malicious external domains via x-forwarded-host', () => {
362
361
  (mockRequest.headers.get as any).mockImplementation((header: string) => {
363
- if (header === 'host') return 'malicious.com';
362
+ if (header === 'host') return 'example.com';
363
+ if (header === 'x-forwarded-host') return 'malicious.com';
364
364
  return null;
365
365
  });
366
366
 
367
367
  const originalUrl = new URL('http://localhost:3000/auth/callback');
368
368
  const result = correctOIDCUrl(mockRequest, originalUrl);
369
369
 
370
- // Should return original URL and not redirect to malicious.com
371
- expect(result).toBe(originalUrl);
372
- expect(result.toString()).toBe('http://localhost:3000/auth/callback');
370
+ // Should fall back to host header and not redirect to malicious.com
371
+ expect(result.toString()).toBe('http://example.com:3000/auth/callback');
373
372
  });
374
373
 
375
374
  it('should allow redirection to configured domain (example.com)', () => {
@@ -410,9 +409,9 @@ describe('correctOIDCUrl', () => {
410
409
  const originalUrl = new URL('http://localhost:3000/auth/callback');
411
410
  const result = correctOIDCUrl(mockRequest, originalUrl);
412
411
 
413
- // Should return original URL and not redirect to evil.com
414
- expect(result).toBe(originalUrl);
415
- expect(result.toString()).toBe('http://localhost:3000/auth/callback');
412
+ // Should fall back to request host (example.com) and not redirect to evil.com
413
+ expect(result.toString()).toBe('http://example.com:3000/auth/callback');
414
+ expect(result.hostname).not.toBe('evil.com');
416
415
  });
417
416
 
418
417
  it('should allow localhost in development environment', () => {
@@ -437,30 +436,92 @@ describe('correctOIDCUrl', () => {
437
436
  delete process.env.APP_URL;
438
437
 
439
438
  (mockRequest.headers.get as any).mockImplementation((header: string) => {
440
- if (header === 'host') return 'any-domain.com';
439
+ if (header === 'host') return 'example.com';
440
+ if (header === 'x-forwarded-host') return 'any-domain.com';
441
441
  return null;
442
442
  });
443
443
 
444
444
  const originalUrl = new URL('http://localhost:3000/auth/callback');
445
445
  const result = correctOIDCUrl(mockRequest, originalUrl);
446
446
 
447
- // Should return original URL when APP_URL is not configured
448
- expect(result).toBe(originalUrl);
449
- expect(result.toString()).toBe('http://localhost:3000/auth/callback');
447
+ // Should fall back to host header when APP_URL is not configured and forwarded host is present
448
+ expect(result.toString()).toBe('http://example.com:3000/auth/callback');
450
449
  });
451
450
 
452
451
  it('should handle domains that look like subdomains but are not', () => {
453
452
  (mockRequest.headers.get as any).mockImplementation((header: string) => {
454
- if (header === 'host') return 'fakeexample.com'; // Not a subdomain of example.com
453
+ if (header === 'host') return 'example.com';
454
+ if (header === 'x-forwarded-host') return 'fakeexample.com'; // Not a subdomain of example.com
455
455
  return null;
456
456
  });
457
457
 
458
458
  const originalUrl = new URL('http://localhost:3000/auth/callback');
459
459
  const result = correctOIDCUrl(mockRequest, originalUrl);
460
460
 
461
- // Should prevent redirection to fake domain
462
- expect(result).toBe(originalUrl);
463
- expect(result.toString()).toBe('http://localhost:3000/auth/callback');
461
+ // Should fall back to host header
462
+ expect(result.toString()).toBe('http://example.com:3000/auth/callback');
463
+ });
464
+
465
+ it('should reject invalid forwarded protocol', () => {
466
+ (mockRequest.headers.get as any).mockImplementation((header: string) => {
467
+ if (header === 'host') return 'example.com';
468
+ if (header === 'x-forwarded-host') return 'example.com';
469
+ if (header === 'x-forwarded-proto') return 'javascript'; // Invalid protocol
470
+ return null;
471
+ });
472
+
473
+ const originalUrl = new URL('http://localhost:3000/auth/callback');
474
+ const result = correctOIDCUrl(mockRequest, originalUrl);
475
+
476
+ // Should fall back to http protocol from URL
477
+ expect(result.protocol).toBe('http:');
478
+ expect(result.toString()).toBe('http://example.com:3000/auth/callback');
479
+ });
480
+
481
+ it('should handle uppercase in forwarded protocol', () => {
482
+ (mockRequest.headers.get as any).mockImplementation((header: string) => {
483
+ if (header === 'host') return 'example.com';
484
+ if (header === 'x-forwarded-host') return 'example.com';
485
+ if (header === 'x-forwarded-proto') return 'HTTPS'; // Uppercase
486
+ return null;
487
+ });
488
+
489
+ const originalUrl = new URL('http://localhost:3000/auth/callback');
490
+ const result = correctOIDCUrl(mockRequest, originalUrl);
491
+
492
+ // Should normalize to lowercase
493
+ expect(result.protocol).toBe('https:');
494
+ expect(result.toString()).toBe('https://example.com:3000/auth/callback');
495
+ });
496
+
497
+ it('should handle multiple hosts in x-forwarded-host', () => {
498
+ (mockRequest.headers.get as any).mockImplementation((header: string) => {
499
+ if (header === 'host') return 'internal.com';
500
+ if (header === 'x-forwarded-host') return 'example.com,attacker.com'; // Multiple hosts
501
+ return null;
502
+ });
503
+
504
+ const originalUrl = new URL('http://localhost:3000/auth/callback');
505
+ const result = correctOIDCUrl(mockRequest, originalUrl);
506
+
507
+ // Should use the first (leftmost) host
508
+ expect(result.hostname).toBe('example.com');
509
+ expect(result.toString()).toBe('http://example.com:3000/auth/callback');
510
+ });
511
+
512
+ it('should fall back to request host when forwarded host is invalid', () => {
513
+ (mockRequest.headers.get as any).mockImplementation((header: string) => {
514
+ if (header === 'host') return 'example.com';
515
+ if (header === 'x-forwarded-host') return 'evil.com'; // Invalid
516
+ return null;
517
+ });
518
+
519
+ const originalUrl = new URL('http://localhost:3000/auth/callback');
520
+ const result = correctOIDCUrl(mockRequest, originalUrl);
521
+
522
+ // Should fall back to request host
523
+ expect(result.hostname).toBe('example.com');
524
+ expect(result.toString()).toBe('http://example.com:3000/auth/callback');
464
525
  });
465
526
  });
466
527
  });