@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.
- package/.github/workflows/claude-auto-testing.yml +6 -3
- package/.github/workflows/claude-dedupe-issues.yml +8 -1
- package/.github/workflows/claude-issue-triage.yml +8 -14
- package/.github/workflows/claude-translate-comments.yml +6 -3
- package/.github/workflows/claude-translator.yml +12 -14
- package/.github/workflows/claude.yml +10 -20
- package/.github/workflows/test.yml +26 -0
- package/CHANGELOG.md +58 -0
- package/changelog/v1.json +18 -0
- package/e2e/package.json +1 -1
- package/e2e/src/mocks/index.ts +2 -2
- package/e2e/src/steps/{discover → community}/detail-pages.steps.ts +8 -8
- package/e2e/src/steps/{discover → community}/interactions.steps.ts +4 -4
- package/locales/zh-CN/components.json +1 -0
- package/package.json +3 -3
- package/packages/const/src/index.ts +0 -1
- package/packages/memory-user-memory/package.json +1 -0
- package/packages/memory-user-memory/src/extractors/context.test.ts +3 -2
- package/packages/memory-user-memory/src/extractors/experience.test.ts +3 -2
- package/packages/memory-user-memory/src/extractors/identity.test.ts +23 -6
- package/packages/memory-user-memory/src/extractors/preference.test.ts +3 -2
- package/packages/memory-user-memory/vitest.config.ts +4 -0
- package/packages/model-runtime/src/providers/replicate/index.ts +1 -1
- package/packages/ssrf-safe-fetch/index.test.ts +2 -2
- package/packages/ssrf-safe-fetch/package.json +3 -2
- package/packages/types/src/serverConfig.ts +2 -0
- package/packages/utils/package.json +1 -1
- package/packages/utils/src/client/xor-obfuscation.test.ts +32 -32
- package/packages/utils/src/client/xor-obfuscation.ts +3 -4
- package/packages/utils/src/imageToBase64.ts +1 -1
- package/packages/utils/src/server/__tests__/auth.test.ts +1 -1
- package/packages/utils/src/server/auth.ts +1 -1
- package/packages/utils/src/server/correctOIDCUrl.test.ts +80 -19
- package/packages/utils/src/server/correctOIDCUrl.ts +89 -24
- package/packages/utils/src/server/index.ts +1 -0
- package/packages/utils/src/server/xor.test.ts +9 -7
- package/packages/utils/src/server/xor.ts +1 -1
- package/packages/web-crawler/package.json +1 -1
- package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +1 -1
- package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
- package/scripts/prebuild.mts +58 -1
- package/src/app/(backend)/api/auth/[...all]/route.ts +2 -1
- package/src/app/(backend)/middleware/auth/index.ts +3 -3
- package/src/app/(backend)/middleware/auth/utils.test.ts +1 -1
- package/src/app/(backend)/middleware/auth/utils.ts +1 -1
- package/src/app/(backend)/oidc/callback/desktop/route.ts +7 -36
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +2 -2
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +1 -1
- package/src/app/(backend)/webapi/plugin/gateway/route.ts +1 -1
- package/src/app/(backend)/webapi/proxy/route.ts +1 -1
- package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +1 -1
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +1 -1
- package/src/app/[variants]/(auth)/signin/layout.tsx +1 -1
- package/src/app/[variants]/(auth)/signin/useSignIn.ts +2 -2
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -1
- package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +12 -6
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +1 -1
- package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -1
- package/src/app/[variants]/(main)/settings/security/index.tsx +1 -1
- package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +1 -1
- package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +1 -1
- package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -1
- package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +1 -1
- package/src/components/ModelSelect/index.tsx +103 -72
- package/src/envs/auth.ts +30 -9
- package/src/features/Conversation/Messages/AssistantGroup/components/EditState.tsx +15 -32
- package/src/features/Conversation/Messages/AssistantGroup/index.tsx +9 -7
- package/src/features/EditorModal/EditorCanvas.tsx +31 -50
- package/src/features/EditorModal/TextareCanvas.tsx +3 -1
- package/src/features/EditorModal/index.tsx +14 -4
- package/src/features/ModelSwitchPanel/components/Footer.tsx +42 -0
- package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +103 -0
- package/src/features/ModelSwitchPanel/components/List/SingleProviderModelItem.tsx +24 -0
- package/src/features/ModelSwitchPanel/components/List/VirtualItemRenderer.tsx +180 -0
- package/src/features/ModelSwitchPanel/components/List/index.tsx +99 -0
- package/src/features/ModelSwitchPanel/components/PanelContent.tsx +77 -0
- package/src/features/ModelSwitchPanel/components/Toolbar.tsx +54 -0
- package/src/features/ModelSwitchPanel/const.ts +29 -0
- package/src/features/ModelSwitchPanel/hooks/useBuildVirtualItems.ts +122 -0
- package/src/features/ModelSwitchPanel/hooks/useCurrentModelName.ts +18 -0
- package/src/features/ModelSwitchPanel/hooks/useDelayedRender.ts +18 -0
- package/src/features/ModelSwitchPanel/hooks/useModelAndProvider.ts +14 -0
- package/src/features/ModelSwitchPanel/hooks/usePanelHandlers.ts +33 -0
- package/src/features/ModelSwitchPanel/hooks/usePanelSize.ts +33 -0
- package/src/features/ModelSwitchPanel/hooks/usePanelState.ts +20 -0
- package/src/features/ModelSwitchPanel/index.tsx +25 -706
- package/src/features/ModelSwitchPanel/styles.ts +58 -0
- package/src/features/ModelSwitchPanel/types.ts +73 -0
- package/src/features/ModelSwitchPanel/utils.ts +24 -0
- package/src/features/User/UserPanel/PanelContent.tsx +1 -1
- package/src/features/User/__tests__/PanelContent.test.tsx +1 -1
- package/src/features/User/__tests__/UserAvatar.test.tsx +1 -1
- package/src/features/User/__tests__/useMenu.test.tsx +1 -1
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -1
- package/src/libs/better-auth/auth-client.ts +7 -3
- package/src/libs/better-auth/define-config.ts +2 -2
- package/src/libs/next/proxy/define-config.ts +9 -6
- package/src/libs/oidc-provider/provider.test.ts +1 -1
- package/src/libs/trpc/async/context.ts +1 -1
- package/src/libs/trpc/lambda/context.ts +7 -8
- package/src/libs/trpc/middleware/userAuth.ts +1 -1
- package/src/libs/trusted-client/getSessionUser.ts +1 -1
- package/src/locales/default/components.ts +1 -0
- package/src/server/globalConfig/index.ts +2 -0
- package/src/server/routers/async/caller.ts +1 -1
- package/src/server/routers/lambda/__tests__/user.test.ts +2 -2
- package/src/server/routers/lambda/user.ts +2 -1
- package/src/services/_auth.ts +3 -3
- package/src/services/chat/index.ts +1 -1
- package/src/services/chat/mecha/contextEngineering.ts +1 -1
- package/src/store/global/initialState.ts +10 -0
- package/src/store/global/selectors/systemStatus.ts +5 -0
- package/src/store/serverConfig/selectors.ts +5 -1
- package/src/store/tool/slices/mcpStore/action.ts +74 -75
- package/src/store/user/slices/auth/action.test.ts +1 -1
- package/src/store/user/slices/auth/action.ts +1 -1
- package/src/store/user/slices/auth/initialState.ts +1 -1
- package/src/store/user/slices/auth/selectors.test.ts +1 -1
- package/src/store/user/slices/auth/selectors.ts +1 -1
- package/src/store/user/slices/common/action.ts +1 -1
- package/src/store/userMemory/slices/context/action.ts +6 -6
- package/packages/const/src/auth.ts +0 -14
- /package/e2e/src/features/{discover → community}/detail-pages.feature +0 -0
- /package/e2e/src/features/{discover → community}/interactions.feature +0 -0
- /package/e2e/src/features/{discover → community}/smoke.feature +0 -0
- /package/e2e/src/mocks/{discover → community}/data.ts +0 -0
- /package/e2e/src/mocks/{discover → community}/handlers.ts +0 -0
- /package/e2e/src/mocks/{discover → community}/index.ts +0 -0
- /package/e2e/src/mocks/{discover → community}/types.ts +0 -0
- /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 = '
|
|
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 = '
|
|
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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { SECRET_XOR_KEY } from '@/
|
|
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(
|
|
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();
|
|
@@ -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
|
|
342
|
-
expect(result).toBe(
|
|
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 '
|
|
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
|
|
371
|
-
expect(result).toBe(
|
|
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
|
|
414
|
-
expect(result).toBe(
|
|
415
|
-
expect(result.
|
|
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 '
|
|
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
|
|
448
|
-
expect(result).toBe(
|
|
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 '
|
|
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
|
|
462
|
-
expect(result).toBe(
|
|
463
|
-
|
|
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
|
});
|