@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/auth.json +54 -0
  4. package/locales/bg-BG/auth.json +54 -0
  5. package/locales/de-DE/auth.json +54 -0
  6. package/locales/en-US/auth.json +54 -0
  7. package/locales/es-ES/auth.json +54 -0
  8. package/locales/fa-IR/auth.json +54 -0
  9. package/locales/fr-FR/auth.json +54 -0
  10. package/locales/it-IT/auth.json +54 -0
  11. package/locales/ja-JP/auth.json +54 -0
  12. package/locales/ko-KR/auth.json +54 -0
  13. package/locales/nl-NL/auth.json +54 -0
  14. package/locales/pl-PL/auth.json +54 -0
  15. package/locales/pt-BR/auth.json +54 -0
  16. package/locales/ru-RU/auth.json +54 -0
  17. package/locales/tr-TR/auth.json +54 -0
  18. package/locales/vi-VN/auth.json +54 -0
  19. package/locales/zh-CN/auth.json +54 -0
  20. package/locales/zh-TW/auth.json +54 -0
  21. package/package.json +2 -2
  22. package/src/app/(backend)/middleware/auth/index.test.ts +5 -5
  23. package/src/app/(backend)/middleware/auth/index.ts +6 -6
  24. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +11 -9
  25. package/src/app/(backend)/webapi/plugin/gateway/route.ts +2 -2
  26. package/src/app/sitemap.tsx +1 -10
  27. package/src/config/aiModels/giteeai.ts +269 -2
  28. package/src/config/aiModels/siliconcloud.ts +24 -2
  29. package/src/config/aiModels/stepfun.ts +67 -2
  30. package/src/config/aiModels/volcengine.ts +56 -2
  31. package/src/config/aiModels/wenxin.ts +62 -2
  32. package/src/config/aiModels/xai.ts +19 -2
  33. package/src/const/auth.ts +2 -3
  34. package/src/libs/model-runtime/ModelRuntime.test.ts +3 -3
  35. package/src/libs/trpc/async/context.ts +3 -3
  36. package/src/libs/trpc/edge/context.ts +7 -2
  37. package/src/libs/trpc/edge/middleware/jwtPayload.test.ts +4 -4
  38. package/src/libs/trpc/edge/middleware/jwtPayload.ts +2 -2
  39. package/src/libs/trpc/lambda/context.ts +2 -2
  40. package/src/libs/trpc/lambda/middleware/keyVaults.ts +2 -2
  41. package/src/server/modules/AgentRuntime/index.test.ts +28 -25
  42. package/src/server/modules/AgentRuntime/index.ts +3 -3
  43. package/src/server/routers/async/caller.ts +2 -2
  44. package/src/server/routers/lambda/market/index.ts +0 -14
  45. package/src/server/routers/tools/search.test.ts +2 -2
  46. package/src/server/services/chunk/index.ts +3 -3
  47. package/src/server/services/discover/index.ts +0 -13
  48. package/src/server/sitemap.test.ts +0 -52
  49. package/src/server/sitemap.ts +1 -38
  50. package/src/services/_auth.ts +3 -3
  51. package/src/services/discover.ts +0 -4
  52. package/src/store/discover/slices/mcp/action.ts +0 -8
  53. package/src/utils/client/xor-obfuscation.test.ts +370 -0
  54. package/src/utils/client/xor-obfuscation.ts +39 -0
  55. package/src/utils/server/xor.test.ts +123 -0
  56. package/src/utils/server/xor.ts +42 -0
  57. package/src/utils/jwt.test.ts +0 -27
  58. package/src/utils/jwt.ts +0 -37
  59. package/src/utils/server/jwt.test.ts +0 -62
  60. 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
@@ -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, mcpPages, modelPages] = await Promise.all([
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
 
@@ -1,4 +1,4 @@
1
- import { JWTPayload, LOBE_CHAT_AUTH_HEADER } from '@/const/auth';
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 { createJWT } from '@/utils/jwt';
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 createJWT<JWTPayload>({ accessCode, userId, ...payload });
83
+ return obfuscatePayloadWithXOR<ClientSecretPayload>({ accessCode, userId, ...payload });
84
84
  };
85
85
 
86
86
  interface AuthParams {
@@ -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
+ };