@lobehub/chat 1.105.1 → 1.105.2
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/CHANGELOG.md +35 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/auth.json +54 -0
- package/locales/bg-BG/auth.json +54 -0
- package/locales/de-DE/auth.json +54 -0
- package/locales/en-US/auth.json +54 -0
- package/locales/es-ES/auth.json +54 -0
- package/locales/fa-IR/auth.json +54 -0
- package/locales/fr-FR/auth.json +54 -0
- package/locales/it-IT/auth.json +54 -0
- package/locales/ja-JP/auth.json +54 -0
- package/locales/ko-KR/auth.json +54 -0
- package/locales/nl-NL/auth.json +54 -0
- package/locales/pl-PL/auth.json +54 -0
- package/locales/pt-BR/auth.json +54 -0
- package/locales/ru-RU/auth.json +54 -0
- package/locales/tr-TR/auth.json +54 -0
- package/locales/vi-VN/auth.json +54 -0
- package/locales/zh-CN/auth.json +54 -0
- package/locales/zh-TW/auth.json +54 -0
- package/package.json +2 -2
- package/src/app/(backend)/middleware/auth/index.test.ts +5 -5
- package/src/app/(backend)/middleware/auth/index.ts +6 -6
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +11 -9
- package/src/app/(backend)/webapi/plugin/gateway/route.ts +2 -2
- package/src/app/sitemap.tsx +1 -10
- package/src/config/aiModels/giteeai.ts +269 -2
- package/src/config/aiModels/siliconcloud.ts +24 -2
- package/src/config/aiModels/stepfun.ts +67 -2
- package/src/config/aiModels/volcengine.ts +56 -2
- package/src/config/aiModels/wenxin.ts +62 -2
- package/src/config/aiModels/xai.ts +19 -2
- package/src/const/auth.ts +2 -3
- package/src/libs/model-runtime/ModelRuntime.test.ts +3 -3
- package/src/libs/trpc/async/context.ts +3 -3
- package/src/libs/trpc/edge/context.ts +7 -2
- package/src/libs/trpc/edge/middleware/jwtPayload.test.ts +4 -4
- package/src/libs/trpc/edge/middleware/jwtPayload.ts +2 -2
- package/src/libs/trpc/lambda/context.ts +2 -2
- package/src/libs/trpc/lambda/middleware/keyVaults.ts +2 -2
- package/src/server/modules/AgentRuntime/index.test.ts +28 -25
- package/src/server/modules/AgentRuntime/index.ts +3 -3
- package/src/server/routers/async/caller.ts +2 -2
- package/src/server/routers/lambda/market/index.ts +0 -14
- package/src/server/routers/tools/search.test.ts +2 -2
- package/src/server/services/chunk/index.ts +3 -3
- package/src/server/services/discover/index.ts +0 -13
- package/src/server/sitemap.test.ts +0 -52
- package/src/server/sitemap.ts +1 -38
- package/src/services/_auth.ts +3 -3
- package/src/services/discover.ts +0 -4
- package/src/store/discover/slices/mcp/action.ts +0 -8
- package/src/utils/client/xor-obfuscation.test.ts +370 -0
- package/src/utils/client/xor-obfuscation.ts +39 -0
- package/src/utils/server/xor.test.ts +123 -0
- package/src/utils/server/xor.ts +42 -0
- package/src/utils/jwt.test.ts +0 -27
- package/src/utils/jwt.ts +0 -37
- package/src/utils/server/jwt.test.ts +0 -62
- package/src/utils/server/jwt.ts +0 -28
@@ -13,7 +13,6 @@ describe('Sitemap', () => {
|
|
13
13
|
// Mock the page count methods to return specific values for testing
|
14
14
|
vi.spyOn(sitemap, 'getPluginPageCount').mockResolvedValue(2);
|
15
15
|
vi.spyOn(sitemap, 'getAssistantPageCount').mockResolvedValue(3);
|
16
|
-
vi.spyOn(sitemap, 'getMcpPageCount').mockResolvedValue(1);
|
17
16
|
vi.spyOn(sitemap, 'getModelPageCount').mockResolvedValue(2);
|
18
17
|
|
19
18
|
const index = await sitemap.getIndex();
|
@@ -31,7 +30,6 @@ describe('Sitemap', () => {
|
|
31
30
|
expect(index).toContain(`<loc>${getCanonicalUrl('/sitemap/assistants-1.xml')}</loc>`);
|
32
31
|
expect(index).toContain(`<loc>${getCanonicalUrl('/sitemap/assistants-2.xml')}</loc>`);
|
33
32
|
expect(index).toContain(`<loc>${getCanonicalUrl('/sitemap/assistants-3.xml')}</loc>`);
|
34
|
-
expect(index).toContain(`<loc>${getCanonicalUrl('/sitemap/mcp-1.xml')}</loc>`);
|
35
33
|
expect(index).toContain(`<loc>${getCanonicalUrl('/sitemap/models-1.xml')}</loc>`);
|
36
34
|
expect(index).toContain(`<loc>${getCanonicalUrl('/sitemap/models-2.xml')}</loc>`);
|
37
35
|
|
@@ -218,46 +216,6 @@ describe('Sitemap', () => {
|
|
218
216
|
});
|
219
217
|
});
|
220
218
|
|
221
|
-
describe('getMcp', () => {
|
222
|
-
it('should return a valid mcp sitemap without pagination', async () => {
|
223
|
-
vi.spyOn(sitemap['discoverService'], 'getMcpIdentifiers').mockResolvedValue([
|
224
|
-
// @ts-ignore
|
225
|
-
{ identifier: 'test-mcp', lastModified: '2023-01-01' },
|
226
|
-
]);
|
227
|
-
|
228
|
-
const mcpSitemap = await sitemap.getMcp();
|
229
|
-
expect(mcpSitemap.length).toBe(15);
|
230
|
-
expect(mcpSitemap).toContainEqual(
|
231
|
-
expect.objectContaining({
|
232
|
-
url: getCanonicalUrl('/discover/mcp/test-mcp'),
|
233
|
-
lastModified: '2023-01-01T00:00:00.000Z',
|
234
|
-
}),
|
235
|
-
);
|
236
|
-
expect(mcpSitemap).toContainEqual(
|
237
|
-
expect.objectContaining({
|
238
|
-
url: getCanonicalUrl('/discover/mcp/test-mcp?hl=zh-CN'),
|
239
|
-
lastModified: '2023-01-01T00:00:00.000Z',
|
240
|
-
}),
|
241
|
-
);
|
242
|
-
});
|
243
|
-
|
244
|
-
it('should return a valid mcp sitemap with pagination', async () => {
|
245
|
-
const mockMcps = Array.from({ length: 80 }, (_, i) => ({
|
246
|
-
identifier: `test-mcp-${i}`,
|
247
|
-
lastModified: '2023-01-01',
|
248
|
-
}));
|
249
|
-
|
250
|
-
vi.spyOn(sitemap['discoverService'], 'getMcpIdentifiers').mockResolvedValue(
|
251
|
-
// @ts-ignore
|
252
|
-
mockMcps,
|
253
|
-
);
|
254
|
-
|
255
|
-
// Test first page (should have 80 items, all on first page)
|
256
|
-
const firstPageSitemap = await sitemap.getMcp(1);
|
257
|
-
expect(firstPageSitemap.length).toBe(80 * 15); // 80 items * 15 locales
|
258
|
-
});
|
259
|
-
});
|
260
|
-
|
261
219
|
describe('getProviders', () => {
|
262
220
|
it('should return a valid providers sitemap', async () => {
|
263
221
|
vi.spyOn(sitemap['discoverService'], 'getProviderIdentifiers').mockResolvedValue([
|
@@ -303,16 +261,6 @@ describe('Sitemap', () => {
|
|
303
261
|
expect(pageCount).toBe(3); // 250 items / 100 per page = ceil(2.5) = 3 pages
|
304
262
|
});
|
305
263
|
|
306
|
-
it('should return correct mcp page count', async () => {
|
307
|
-
vi.spyOn(sitemap['discoverService'], 'getMcpIdentifiers').mockResolvedValue(
|
308
|
-
// @ts-ignore
|
309
|
-
Array.from({ length: 50 }, (_, i) => ({ identifier: `mcp-${i}` })),
|
310
|
-
);
|
311
|
-
|
312
|
-
const pageCount = await sitemap.getMcpPageCount();
|
313
|
-
expect(pageCount).toBe(1); // 50 items / 100 per page = 1 page
|
314
|
-
});
|
315
|
-
|
316
264
|
it('should return correct model page count', async () => {
|
317
265
|
vi.spyOn(sitemap['discoverService'], 'getModelIdentifiers').mockResolvedValue(
|
318
266
|
// @ts-ignore
|
package/src/server/sitemap.ts
CHANGED
@@ -52,12 +52,6 @@ export class Sitemap {
|
|
52
52
|
return Math.ceil(list.length / ITEMS_PER_PAGE);
|
53
53
|
}
|
54
54
|
|
55
|
-
// 获取MCP总页数
|
56
|
-
async getMcpPageCount(): Promise<number> {
|
57
|
-
const list = await this.discoverService.getMcpIdentifiers();
|
58
|
-
return Math.ceil(list.length / ITEMS_PER_PAGE);
|
59
|
-
}
|
60
|
-
|
61
55
|
// 获取模型总页数
|
62
56
|
async getModelPageCount(): Promise<number> {
|
63
57
|
const list = await this.discoverService.getModelIdentifiers();
|
@@ -171,10 +165,9 @@ export class Sitemap {
|
|
171
165
|
);
|
172
166
|
|
173
167
|
// 获取需要分页的类型的页数
|
174
|
-
const [pluginPages, assistantPages,
|
168
|
+
const [pluginPages, assistantPages, modelPages] = await Promise.all([
|
175
169
|
this.getPluginPageCount(),
|
176
170
|
this.getAssistantPageCount(),
|
177
|
-
this.getMcpPageCount(),
|
178
171
|
this.getModelPageCount(),
|
179
172
|
]);
|
180
173
|
|
@@ -193,11 +186,6 @@ export class Sitemap {
|
|
193
186
|
),
|
194
187
|
),
|
195
188
|
),
|
196
|
-
...Array.from({ length: mcpPages }, (_, i) =>
|
197
|
-
this._generateSitemapLink(
|
198
|
-
getCanonicalUrl(SITEMAP_BASE_URL, isDev ? `mcp-${i + 1}` : `mcp-${i + 1}.xml`),
|
199
|
-
),
|
200
|
-
),
|
201
189
|
...Array.from({ length: modelPages }, (_, i) =>
|
202
190
|
this._generateSitemapLink(
|
203
191
|
getCanonicalUrl(SITEMAP_BASE_URL, isDev ? `models-${i + 1}` : `models-${i + 1}.xml`),
|
@@ -239,31 +227,6 @@ export class Sitemap {
|
|
239
227
|
return flatten(sitmap);
|
240
228
|
}
|
241
229
|
|
242
|
-
async getMcp(page?: number): Promise<MetadataRoute.Sitemap> {
|
243
|
-
const list = await this.discoverService.getMcpIdentifiers();
|
244
|
-
|
245
|
-
if (page !== undefined) {
|
246
|
-
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
247
|
-
const endIndex = startIndex + ITEMS_PER_PAGE;
|
248
|
-
const pageMcps = list.slice(startIndex, endIndex);
|
249
|
-
|
250
|
-
const sitmap = pageMcps.map((item) =>
|
251
|
-
this._genSitemap(urlJoin('/discover/mcp', item.identifier), {
|
252
|
-
lastModified: item?.lastModified || LAST_MODIFIED,
|
253
|
-
}),
|
254
|
-
);
|
255
|
-
return flatten(sitmap);
|
256
|
-
}
|
257
|
-
|
258
|
-
// 如果没有指定页数,返回所有(向后兼容)
|
259
|
-
const sitmap = list.map((item) =>
|
260
|
-
this._genSitemap(urlJoin('/discover/mcp', item.identifier), {
|
261
|
-
lastModified: item?.lastModified || LAST_MODIFIED,
|
262
|
-
}),
|
263
|
-
);
|
264
|
-
return flatten(sitmap);
|
265
|
-
}
|
266
|
-
|
267
230
|
async getPlugins(page?: number): Promise<MetadataRoute.Sitemap> {
|
268
231
|
const list = await this.discoverService.getPluginIdentifiers();
|
269
232
|
|
package/src/services/_auth.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { ClientSecretPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
|
2
2
|
import { isDeprecatedEdition } from '@/const/version';
|
3
3
|
import { ModelProvider } from '@/libs/model-runtime';
|
4
4
|
import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
|
@@ -10,7 +10,7 @@ import {
|
|
10
10
|
CloudflareKeyVault,
|
11
11
|
OpenAICompatibleKeyVault,
|
12
12
|
} from '@/types/user/settings';
|
13
|
-
import {
|
13
|
+
import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation';
|
14
14
|
|
15
15
|
export const getProviderAuthPayload = (
|
16
16
|
provider: string,
|
@@ -80,7 +80,7 @@ const createAuthTokenWithPayload = async (payload = {}) => {
|
|
80
80
|
const accessCode = keyVaultsConfigSelectors.password(useUserStore.getState());
|
81
81
|
const userId = userProfileSelectors.userId(useUserStore.getState());
|
82
82
|
|
83
|
-
return
|
83
|
+
return obfuscatePayloadWithXOR<ClientSecretPayload>({ accessCode, userId, ...payload });
|
84
84
|
};
|
85
85
|
|
86
86
|
interface AuthParams {
|
package/src/services/discover.ts
CHANGED
@@ -83,10 +83,6 @@ class DiscoverService {
|
|
83
83
|
});
|
84
84
|
};
|
85
85
|
|
86
|
-
getMcpIdentifiers = async (): Promise<IdentifiersResponse> => {
|
87
|
-
return lambdaClient.market.getMcpIdentifiers.query();
|
88
|
-
};
|
89
|
-
|
90
86
|
getMcpList = async (params: McpQueryParams = {}): Promise<McpListResponse> => {
|
91
87
|
const locale = globalHelpers.getCurrentLanguage();
|
92
88
|
return lambdaClient.market.getMcpList.query({
|
@@ -8,7 +8,6 @@ import { DiscoverStore } from '@/store/discover';
|
|
8
8
|
import { globalHelpers } from '@/store/global/helpers';
|
9
9
|
import {
|
10
10
|
DiscoverMcpDetail,
|
11
|
-
IdentifiersResponse,
|
12
11
|
McpListResponse,
|
13
12
|
McpQueryParams,
|
14
13
|
} from '@/types/discover';
|
@@ -20,7 +19,6 @@ export interface MCPAction {
|
|
20
19
|
}) => SWRResponse<DiscoverMcpDetail>;
|
21
20
|
useFetchMcpList: (params: McpQueryParams) => SWRResponse<McpListResponse>;
|
22
21
|
useMcpCategories: (params: CategoryListQuery) => SWRResponse<CategoryItem[]>;
|
23
|
-
useMcpIdentifiers: () => SWRResponse<IdentifiersResponse>;
|
24
22
|
}
|
25
23
|
|
26
24
|
export const createMCPSlice: StateCreator<
|
@@ -61,10 +59,4 @@ export const createMCPSlice: StateCreator<
|
|
61
59
|
},
|
62
60
|
);
|
63
61
|
},
|
64
|
-
|
65
|
-
useMcpIdentifiers: () => {
|
66
|
-
return useClientDataSWR('mcp-identifiers', async () => discoverService.getMcpIdentifiers(), {
|
67
|
-
revalidateOnFocus: false,
|
68
|
-
});
|
69
|
-
},
|
70
62
|
});
|
@@ -0,0 +1,370 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
|
3
|
+
import { SECRET_XOR_KEY } from '@/const/auth';
|
4
|
+
|
5
|
+
import { obfuscatePayloadWithXOR } from './xor-obfuscation';
|
6
|
+
|
7
|
+
describe('xor-obfuscation', () => {
|
8
|
+
describe('obfuscatePayloadWithXOR', () => {
|
9
|
+
it('应该对简单字符串进行混淆并返回Base64字符串', () => {
|
10
|
+
const payload = 'hello world';
|
11
|
+
const result = obfuscatePayloadWithXOR(payload);
|
12
|
+
|
13
|
+
// 验证返回值是字符串
|
14
|
+
expect(typeof result).toBe('string');
|
15
|
+
|
16
|
+
// 验证返回值是有效的Base64字符串
|
17
|
+
expect(() => atob(result)).not.toThrow();
|
18
|
+
|
19
|
+
// 验证结果长度大于0
|
20
|
+
expect(result.length).toBeGreaterThan(0);
|
21
|
+
});
|
22
|
+
|
23
|
+
it('应该对JSON对象进行混淆', () => {
|
24
|
+
const payload = { name: 'test', value: 123, active: true };
|
25
|
+
const result = obfuscatePayloadWithXOR(payload);
|
26
|
+
|
27
|
+
// 验证返回值是字符串
|
28
|
+
expect(typeof result).toBe('string');
|
29
|
+
|
30
|
+
// 验证返回值是有效的Base64字符串
|
31
|
+
expect(() => atob(result)).not.toThrow();
|
32
|
+
});
|
33
|
+
|
34
|
+
it('应该对数组进行混淆', () => {
|
35
|
+
const payload = [1, 2, 3, 'test', { nested: true }];
|
36
|
+
const result = obfuscatePayloadWithXOR(payload);
|
37
|
+
|
38
|
+
// 验证返回值是字符串
|
39
|
+
expect(typeof result).toBe('string');
|
40
|
+
|
41
|
+
// 验证返回值是有效的Base64字符串
|
42
|
+
expect(() => atob(result)).not.toThrow();
|
43
|
+
});
|
44
|
+
|
45
|
+
it('应该对复杂嵌套对象进行混淆', () => {
|
46
|
+
const payload = {
|
47
|
+
user: {
|
48
|
+
id: 123,
|
49
|
+
profile: {
|
50
|
+
name: 'John Doe',
|
51
|
+
settings: {
|
52
|
+
theme: 'dark',
|
53
|
+
notifications: true,
|
54
|
+
preferences: ['email', 'sms'],
|
55
|
+
},
|
56
|
+
},
|
57
|
+
},
|
58
|
+
tokens: ['abc123', 'def456'],
|
59
|
+
metadata: null,
|
60
|
+
};
|
61
|
+
const result = obfuscatePayloadWithXOR(payload);
|
62
|
+
|
63
|
+
// 验证返回值是字符串
|
64
|
+
expect(typeof result).toBe('string');
|
65
|
+
|
66
|
+
// 验证返回值是有效的Base64字符串
|
67
|
+
expect(() => atob(result)).not.toThrow();
|
68
|
+
});
|
69
|
+
|
70
|
+
it('相同的输入应该产生相同的输出', () => {
|
71
|
+
const payload = { test: 'consistent' };
|
72
|
+
const result1 = obfuscatePayloadWithXOR(payload);
|
73
|
+
const result2 = obfuscatePayloadWithXOR(payload);
|
74
|
+
|
75
|
+
expect(result1).toBe(result2);
|
76
|
+
});
|
77
|
+
|
78
|
+
it('不同的输入应该产生不同的输出', () => {
|
79
|
+
const payload1 = { test: 'value1' };
|
80
|
+
const payload2 = { test: 'value2' };
|
81
|
+
|
82
|
+
const result1 = obfuscatePayloadWithXOR(payload1);
|
83
|
+
const result2 = obfuscatePayloadWithXOR(payload2);
|
84
|
+
|
85
|
+
expect(result1).not.toBe(result2);
|
86
|
+
});
|
87
|
+
|
88
|
+
it('应该处理包含特殊字符的字符串', () => {
|
89
|
+
const payload = 'Hello! @#$%^&*()_+-=[]{}|;:,.<>?/~`"\'\\';
|
90
|
+
const result = obfuscatePayloadWithXOR(payload);
|
91
|
+
|
92
|
+
// 验证返回值是字符串
|
93
|
+
expect(typeof result).toBe('string');
|
94
|
+
|
95
|
+
// 验证返回值是有效的Base64字符串
|
96
|
+
expect(() => atob(result)).not.toThrow();
|
97
|
+
});
|
98
|
+
|
99
|
+
it('应该处理包含Unicode字符的字符串', () => {
|
100
|
+
const payload = '你好世界 🌍 émojis 日本語 한국어';
|
101
|
+
const result = obfuscatePayloadWithXOR(payload);
|
102
|
+
|
103
|
+
// 验证返回值是字符串
|
104
|
+
expect(typeof result).toBe('string');
|
105
|
+
|
106
|
+
// 验证返回值是有效的Base64字符串
|
107
|
+
expect(() => atob(result)).not.toThrow();
|
108
|
+
});
|
109
|
+
|
110
|
+
it('应该处理空字符串', () => {
|
111
|
+
const payload = '';
|
112
|
+
const result = obfuscatePayloadWithXOR(payload);
|
113
|
+
|
114
|
+
// 验证返回值是字符串
|
115
|
+
expect(typeof result).toBe('string');
|
116
|
+
|
117
|
+
// 验证返回值是有效的Base64字符串
|
118
|
+
expect(() => atob(result)).not.toThrow();
|
119
|
+
});
|
120
|
+
|
121
|
+
it('应该处理空对象', () => {
|
122
|
+
const payload = {};
|
123
|
+
const result = obfuscatePayloadWithXOR(payload);
|
124
|
+
|
125
|
+
// 验证返回值是字符串
|
126
|
+
expect(typeof result).toBe('string');
|
127
|
+
|
128
|
+
// 验证返回值是有效的Base64字符串
|
129
|
+
expect(() => atob(result)).not.toThrow();
|
130
|
+
});
|
131
|
+
|
132
|
+
it('应该处理空数组', () => {
|
133
|
+
const result = obfuscatePayloadWithXOR([]);
|
134
|
+
|
135
|
+
// 验证返回值是字符串
|
136
|
+
expect(typeof result).toBe('string');
|
137
|
+
|
138
|
+
// 验证返回值是有效的Base64字符串
|
139
|
+
expect(() => atob(result)).not.toThrow();
|
140
|
+
});
|
141
|
+
|
142
|
+
it('应该处理null值', () => {
|
143
|
+
const payload = null;
|
144
|
+
const result = obfuscatePayloadWithXOR(payload);
|
145
|
+
|
146
|
+
// 验证返回值是字符串
|
147
|
+
expect(typeof result).toBe('string');
|
148
|
+
|
149
|
+
// 验证返回值是有效的Base64字符串
|
150
|
+
expect(() => atob(result)).not.toThrow();
|
151
|
+
});
|
152
|
+
|
153
|
+
it('应该处理数字', () => {
|
154
|
+
const payload = 42;
|
155
|
+
const result = obfuscatePayloadWithXOR(payload);
|
156
|
+
|
157
|
+
// 验证返回值是字符串
|
158
|
+
expect(typeof result).toBe('string');
|
159
|
+
|
160
|
+
// 验证返回值是有效的Base64字符串
|
161
|
+
expect(() => atob(result)).not.toThrow();
|
162
|
+
});
|
163
|
+
|
164
|
+
it('应该处理布尔值', () => {
|
165
|
+
const payloadTrue = true;
|
166
|
+
const payloadFalse = false;
|
167
|
+
|
168
|
+
const resultTrue = obfuscatePayloadWithXOR(payloadTrue);
|
169
|
+
const resultFalse = obfuscatePayloadWithXOR(payloadFalse);
|
170
|
+
|
171
|
+
// 验证返回值是字符串
|
172
|
+
expect(typeof resultTrue).toBe('string');
|
173
|
+
expect(typeof resultFalse).toBe('string');
|
174
|
+
|
175
|
+
// 验证返回值是有效的Base64字符串
|
176
|
+
expect(() => atob(resultTrue)).not.toThrow();
|
177
|
+
expect(() => atob(resultFalse)).not.toThrow();
|
178
|
+
|
179
|
+
// 验证不同布尔值产生不同结果
|
180
|
+
expect(resultTrue).not.toBe(resultFalse);
|
181
|
+
});
|
182
|
+
|
183
|
+
it('应该处理包含特殊JSON字符的对象', () => {
|
184
|
+
const payload = {
|
185
|
+
quotes: '"double quotes"',
|
186
|
+
singleQuotes: "'single quotes'",
|
187
|
+
backslash: 'back\\slash',
|
188
|
+
newline: 'line1\nline2',
|
189
|
+
tab: 'col1\tcol2',
|
190
|
+
unicode: '\u0041\u0042\u0043',
|
191
|
+
};
|
192
|
+
const result = obfuscatePayloadWithXOR(payload);
|
193
|
+
|
194
|
+
// 验证返回值是字符串
|
195
|
+
expect(typeof result).toBe('string');
|
196
|
+
|
197
|
+
// 验证返回值是有效的Base64字符串
|
198
|
+
expect(() => atob(result)).not.toThrow();
|
199
|
+
});
|
200
|
+
|
201
|
+
it('应该处理很长的字符串', () => {
|
202
|
+
const payload = 'a'.repeat(10000);
|
203
|
+
const result = obfuscatePayloadWithXOR(payload);
|
204
|
+
|
205
|
+
// 验证返回值是字符串
|
206
|
+
expect(typeof result).toBe('string');
|
207
|
+
|
208
|
+
// 验证返回值是有效的Base64字符串
|
209
|
+
expect(() => atob(result)).not.toThrow();
|
210
|
+
|
211
|
+
// 验证结果长度合理(Base64编码后长度应该大约是原始长度的4/3)
|
212
|
+
expect(result.length).toBeGreaterThan(0);
|
213
|
+
});
|
214
|
+
|
215
|
+
it('应该产生不同长度输入的不同输出长度', () => {
|
216
|
+
const shortPayload = 'short';
|
217
|
+
const longPayload = 'this is a much longer string that should produce different output';
|
218
|
+
|
219
|
+
const shortResult = obfuscatePayloadWithXOR(shortPayload);
|
220
|
+
const longResult = obfuscatePayloadWithXOR(longPayload);
|
221
|
+
|
222
|
+
// 较长的输入应该产生较长的输出
|
223
|
+
expect(longResult.length).toBeGreaterThan(shortResult.length);
|
224
|
+
});
|
225
|
+
|
226
|
+
it('应该验证输出是有效的Base64格式', () => {
|
227
|
+
const payload = { test: 'base64 validation' };
|
228
|
+
const result = obfuscatePayloadWithXOR(payload);
|
229
|
+
|
230
|
+
// 验证Base64格式的正则表达式
|
231
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
232
|
+
expect(base64Regex.test(result)).toBe(true);
|
233
|
+
});
|
234
|
+
|
235
|
+
it('应该处理包含循环引用的对象(通过JSON.stringify处理)', () => {
|
236
|
+
// JSON.stringify 会抛出错误处理循环引用,但我们测试正常情况
|
237
|
+
const payload = {
|
238
|
+
id: 1,
|
239
|
+
name: 'test',
|
240
|
+
nested: {
|
241
|
+
back: 'reference',
|
242
|
+
},
|
243
|
+
};
|
244
|
+
|
245
|
+
const result = obfuscatePayloadWithXOR(payload);
|
246
|
+
expect(typeof result).toBe('string');
|
247
|
+
expect(() => atob(result)).not.toThrow();
|
248
|
+
});
|
249
|
+
|
250
|
+
it('应该对undefined值进行处理', () => {
|
251
|
+
const payload = undefined;
|
252
|
+
const result = obfuscatePayloadWithXOR(payload);
|
253
|
+
|
254
|
+
// 验证返回值是字符串
|
255
|
+
expect(typeof result).toBe('string');
|
256
|
+
|
257
|
+
// 验证返回值是有效的Base64字符串
|
258
|
+
expect(() => atob(result)).not.toThrow();
|
259
|
+
});
|
260
|
+
|
261
|
+
it('应该对包含函数的对象进行处理(函数会被JSON.stringify忽略)', () => {
|
262
|
+
const payload = {
|
263
|
+
name: 'test',
|
264
|
+
fn: function () {
|
265
|
+
return 'test';
|
266
|
+
},
|
267
|
+
arrow: () => 'arrow',
|
268
|
+
value: 123,
|
269
|
+
};
|
270
|
+
|
271
|
+
const result = obfuscatePayloadWithXOR(payload);
|
272
|
+
expect(typeof result).toBe('string');
|
273
|
+
expect(() => atob(result)).not.toThrow();
|
274
|
+
});
|
275
|
+
|
276
|
+
it('应该确保XOR操作的确定性', () => {
|
277
|
+
const payload = 'deterministic test';
|
278
|
+
const results: any[] = [];
|
279
|
+
|
280
|
+
// 多次运行相同输入
|
281
|
+
for (let i = 0; i < 10; i++) {
|
282
|
+
results.push(obfuscatePayloadWithXOR(payload));
|
283
|
+
}
|
284
|
+
|
285
|
+
// 所有结果应该相同
|
286
|
+
expect(results.every((result) => result === results[0])).toBe(true);
|
287
|
+
});
|
288
|
+
|
289
|
+
it('应该处理包含日期对象的数据', () => {
|
290
|
+
const payload = {
|
291
|
+
timestamp: new Date('2024-01-01T00:00:00Z'),
|
292
|
+
created: new Date(),
|
293
|
+
name: 'date test',
|
294
|
+
};
|
295
|
+
|
296
|
+
const result = obfuscatePayloadWithXOR(payload);
|
297
|
+
expect(typeof result).toBe('string');
|
298
|
+
expect(() => atob(result)).not.toThrow();
|
299
|
+
});
|
300
|
+
|
301
|
+
it('应该处理包含Symbol的对象(Symbol会被JSON.stringify忽略)', () => {
|
302
|
+
const sym = Symbol('test');
|
303
|
+
const payload = {
|
304
|
+
name: 'symbol test',
|
305
|
+
[sym]: 'symbol value',
|
306
|
+
normalKey: 'normal value',
|
307
|
+
};
|
308
|
+
|
309
|
+
const result = obfuscatePayloadWithXOR(payload);
|
310
|
+
expect(typeof result).toBe('string');
|
311
|
+
expect(() => atob(result)).not.toThrow();
|
312
|
+
});
|
313
|
+
|
314
|
+
it('应该验证混淆后的数据长度合理性', () => {
|
315
|
+
const originalPayload = { test: 'length check' };
|
316
|
+
const originalJSON = JSON.stringify(originalPayload);
|
317
|
+
const result = obfuscatePayloadWithXOR(originalPayload);
|
318
|
+
|
319
|
+
// Base64 编码后的长度通常是原始长度的 4/3 倍(向上取整到4的倍数)
|
320
|
+
const expectedMinLength = Math.ceil((originalJSON.length * 4) / 3 / 4) * 4;
|
321
|
+
expect(result.length).toBeGreaterThanOrEqual(expectedMinLength - 4); // 允许一些误差
|
322
|
+
});
|
323
|
+
|
324
|
+
it('应该验证XOR操作的正确性(通过逆向操作)', () => {
|
325
|
+
const originalPayload = { message: 'XOR test', value: 42 };
|
326
|
+
const obfuscatedResult = obfuscatePayloadWithXOR(originalPayload);
|
327
|
+
|
328
|
+
// 手动实现逆向操作来验证 XOR 操作的正确性
|
329
|
+
const base64Decoded = atob(obfuscatedResult);
|
330
|
+
const xoredBytes = new Uint8Array(base64Decoded.length);
|
331
|
+
for (let i = 0; i < base64Decoded.length; i++) {
|
332
|
+
xoredBytes[i] = base64Decoded.charCodeAt(i);
|
333
|
+
}
|
334
|
+
|
335
|
+
// 使用相同的密钥进行逆向 XOR 操作
|
336
|
+
const keyBytes = new TextEncoder().encode(SECRET_XOR_KEY);
|
337
|
+
const decodedBytes = new Uint8Array(xoredBytes.length);
|
338
|
+
for (let i = 0; i < xoredBytes.length; i++) {
|
339
|
+
decodedBytes[i] = xoredBytes[i] ^ keyBytes[i % keyBytes.length];
|
340
|
+
}
|
341
|
+
|
342
|
+
// 将结果转换回字符串
|
343
|
+
const decodedString = new TextDecoder().decode(decodedBytes);
|
344
|
+
const decodedPayload = JSON.parse(decodedString);
|
345
|
+
|
346
|
+
// 验证解码后的数据与原始数据相同
|
347
|
+
expect(decodedPayload).toEqual(originalPayload);
|
348
|
+
});
|
349
|
+
|
350
|
+
it('应该验证不同输入产生不同的Base64输出', () => {
|
351
|
+
const payloads = [
|
352
|
+
'test1',
|
353
|
+
'test2',
|
354
|
+
{ key: 'value1' },
|
355
|
+
{ key: 'value2' },
|
356
|
+
[1, 2, 3],
|
357
|
+
[4, 5, 6],
|
358
|
+
];
|
359
|
+
|
360
|
+
const results = payloads.map((payload) => obfuscatePayloadWithXOR(payload));
|
361
|
+
|
362
|
+
// 验证所有结果都不相同
|
363
|
+
for (let i = 0; i < results.length; i++) {
|
364
|
+
for (let j = i + 1; j < results.length; j++) {
|
365
|
+
expect(results[i]).not.toBe(results[j]);
|
366
|
+
}
|
367
|
+
}
|
368
|
+
});
|
369
|
+
});
|
370
|
+
});
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { SECRET_XOR_KEY } from '@/const/auth';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* 将字符串转换为 Uint8Array (UTF-8 编码)
|
5
|
+
*/
|
6
|
+
const stringToUint8Array = (str: string): Uint8Array => {
|
7
|
+
return new TextEncoder().encode(str);
|
8
|
+
};
|
9
|
+
|
10
|
+
/**
|
11
|
+
* 对 Uint8Array 进行 XOR 运算
|
12
|
+
* @param data 要处理的 Uint8Array
|
13
|
+
* @param key 用于 XOR 的密钥 (Uint8Array)
|
14
|
+
* @returns 经过 XOR 运算的 Uint8Array
|
15
|
+
*/
|
16
|
+
const xorProcess = (data: Uint8Array, key: Uint8Array): Uint8Array => {
|
17
|
+
const result = new Uint8Array(data.length);
|
18
|
+
for (const [i, datum] of data.entries()) {
|
19
|
+
result[i] = datum ^ key[i % key.length]; // 密钥循环使用
|
20
|
+
}
|
21
|
+
return result;
|
22
|
+
};
|
23
|
+
|
24
|
+
/**
|
25
|
+
* 对 payload 进行 XOR 混淆并 Base64 编码
|
26
|
+
* @param payload 要混淆的 JSON 对象
|
27
|
+
* @returns Base64 编码后的混淆字符串
|
28
|
+
*/
|
29
|
+
export const obfuscatePayloadWithXOR = <T>(payload: T): string => {
|
30
|
+
const jsonString = JSON.stringify(payload);
|
31
|
+
const dataBytes = stringToUint8Array(jsonString);
|
32
|
+
const keyBytes = stringToUint8Array(SECRET_XOR_KEY);
|
33
|
+
|
34
|
+
const xoredBytes = xorProcess(dataBytes, keyBytes);
|
35
|
+
|
36
|
+
// 将 Uint8Array 转换为 Base64 字符串
|
37
|
+
// 浏览器环境 btoa 只能处理 Latin-1 字符,所以需要先转换为适合 btoa 的字符串
|
38
|
+
return btoa(String.fromCharCode(...xoredBytes));
|
39
|
+
};
|