@lobehub/lobehub 2.0.0-next.49 → 2.0.0-next.50

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.
@@ -38,7 +38,7 @@ export class ToolSystemRoleProvider extends BaseProvider {
38
38
  protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
39
39
  const clonedContext = this.cloneContext(context);
40
40
 
41
- // 检查工具相关条件
41
+ // Check tool-related conditions
42
42
  const toolSystemRole = this.getToolSystemRole();
43
43
 
44
44
  if (!toolSystemRole) {
@@ -46,10 +46,10 @@ export class ToolSystemRoleProvider extends BaseProvider {
46
46
  return this.markAsExecuted(clonedContext);
47
47
  }
48
48
 
49
- // 注入工具系统角色
49
+ // Inject tool system role
50
50
  this.injectToolSystemRole(clonedContext, toolSystemRole);
51
51
 
52
- // 更新元数据
52
+ // Update metadata
53
53
  clonedContext.metadata.toolSystemRole = {
54
54
  contentLength: toolSystemRole.length,
55
55
  injected: true,
@@ -67,21 +67,21 @@ export class ToolSystemRoleProvider extends BaseProvider {
67
67
  private getToolSystemRole(): string | undefined {
68
68
  const { tools, model, provider } = this.config;
69
69
 
70
- // 检查是否有工具
70
+ // Check if tools are available
71
71
  const hasTools = tools && tools.length > 0;
72
72
  if (!hasTools) {
73
73
  log('No available tools');
74
74
  return undefined;
75
75
  }
76
76
 
77
- // 检查是否支持函数调用
77
+ // Check if function calling is supported
78
78
  const hasFC = this.config.isCanUseFC(model, provider);
79
79
  if (!hasFC) {
80
80
  log(`Model ${model} (${provider}) does not support function calling`);
81
81
  return undefined;
82
82
  }
83
83
 
84
- // 获取工具系统角色
84
+ // Get tool system role
85
85
  const toolSystemRole = this.config.getToolSystemRoles(tools);
86
86
  if (!toolSystemRole) {
87
87
  log('Failed to get tool system role content');
@@ -98,7 +98,7 @@ export class ToolSystemRoleProvider extends BaseProvider {
98
98
  const existingSystemMessage = context.messages.find((msg) => msg.role === 'system');
99
99
 
100
100
  if (existingSystemMessage) {
101
- // 合并到现有系统消息
101
+ // Merge to existing system message
102
102
  existingSystemMessage.content = [existingSystemMessage.content, toolSystemRole]
103
103
  .filter(Boolean)
104
104
  .join('\n\n');
@@ -274,28 +274,28 @@ export class ToolsEngine {
274
274
  }
275
275
 
276
276
  /**
277
- * 获取可用的插件列表(用于调试和监控)
277
+ * Get available plugin list (for debugging and monitoring)
278
278
  */
279
279
  getAvailablePlugins(): string[] {
280
280
  return Array.from(this.manifestSchemas.keys());
281
281
  }
282
282
 
283
283
  /**
284
- * 检查特定插件是否可用
284
+ * Check if a specific plugin is available
285
285
  */
286
286
  hasPlugin(pluginId: string): boolean {
287
287
  return this.manifestSchemas.has(pluginId);
288
288
  }
289
289
 
290
290
  /**
291
- * 获取插件的 manifest
291
+ * Get plugin manifest
292
292
  */
293
293
  getPluginManifest(pluginId: string): LobeToolManifest | undefined {
294
294
  return this.manifestSchemas.get(pluginId);
295
295
  }
296
296
 
297
297
  /**
298
- * 更新插件 manifest schemas(用于动态添加插件)
298
+ * Update plugin manifest schemas (for dynamically adding plugins)
299
299
  */
300
300
  updateManifestSchemas(manifestSchemas: LobeToolManifest[]): void {
301
301
  this.manifestSchemas.clear();
@@ -305,21 +305,21 @@ export class ToolsEngine {
305
305
  }
306
306
 
307
307
  /**
308
- * 添加单个插件 manifest
308
+ * Add a single plugin manifest
309
309
  */
310
310
  addPluginManifest(manifest: LobeToolManifest): void {
311
311
  this.manifestSchemas.set(manifest.identifier, manifest);
312
312
  }
313
313
 
314
314
  /**
315
- * 移除插件 manifest
315
+ * Remove plugin manifest
316
316
  */
317
317
  removePluginManifest(pluginId: string): boolean {
318
318
  return this.manifestSchemas.delete(pluginId);
319
319
  }
320
320
 
321
321
  /**
322
- * 获取所有 enabled plugin Manifest Map
322
+ * Get Manifest Map of all enabled plugins
323
323
  */
324
324
  getEnabledPluginManifests(toolIds: string[] = []): Map<string, LobeToolManifest> {
325
325
  // Merge user-provided tool IDs with default tool IDs
@@ -341,7 +341,7 @@ export class ToolsEngine {
341
341
  }
342
342
 
343
343
  /**
344
- * 获取所有插件的 Manifest Map
344
+ * Get Manifest Map of all plugins
345
345
  */
346
346
  getAllPluginManifests(): Map<string, LobeToolManifest> {
347
347
  return new Map(this.manifestSchemas);
@@ -1,7 +1,7 @@
1
1
  import { UIChatMessage } from '@lobechat/types';
2
2
 
3
3
  /**
4
- * 智能体状态 - 从原项目类型推断
4
+ * Agent state - inferred from original project types
5
5
  */
6
6
  export interface AgentState {
7
7
  [key: string]: any;
@@ -13,7 +13,7 @@ export interface AgentState {
13
13
  }
14
14
 
15
15
  /**
16
- * 聊天图像项
16
+ * Chat image item
17
17
  */
18
18
  export interface ChatImageItem {
19
19
  alt?: string;
@@ -22,7 +22,7 @@ export interface ChatImageItem {
22
22
  }
23
23
 
24
24
  /**
25
- * 消息工具调用
25
+ * Message tool call
26
26
  */
27
27
  export interface MessageToolCall {
28
28
  function: {
@@ -39,72 +39,72 @@ export interface Message {
39
39
  }
40
40
 
41
41
  /**
42
- * 管道上下文 - 在管道中流动的核心数据结构
42
+ * Pipeline context - core data structure flowing through the pipeline
43
43
  */
44
44
  export interface PipelineContext {
45
- /** 中止原因 */
45
+ /** Abort reason */
46
46
  abortReason?: string;
47
47
 
48
- /** 不可变的输入状态 */
48
+ /** Immutable input state */
49
49
  readonly initialState: AgentState;
50
50
 
51
- /** 允许处理器提前终止管道 */
51
+ /** Allow processors to terminate pipeline early */
52
52
  isAborted: boolean;
53
53
 
54
- /** 正在构建的可变消息列表 */
54
+ /** Mutable message list being built */
55
55
  messages: Message[];
56
- /** 处理器间通信的元数据 */
56
+ /** Metadata for communication between processors */
57
57
  metadata: {
58
- /** 其他自定义元数据 */
58
+ /** Other custom metadata */
59
59
  [key: string]: any;
60
- /** 当前 token 估算值 */
60
+ /** Current token count estimate */
61
61
  currentTokenCount?: number;
62
- /** 最大 token 限制 */
62
+ /** Maximum token limit */
63
63
  maxTokens?: number;
64
- /** 模型标识 */
64
+ /** Model identifier */
65
65
  model?: string;
66
66
  };
67
67
  }
68
68
 
69
69
  /**
70
- * 上下文处理器接口 - 管道中处理站的标准化接口
70
+ * Context processor interface - standardized interface for processing stations in the pipeline
71
71
  */
72
72
  export interface ContextProcessor {
73
- /** 处理器名称,用于调试和日志 */
73
+ /** Processor name, used for debugging and logging */
74
74
  name: string;
75
- /** 核心处理方法 */
75
+ /** Core processing method */
76
76
  process: (context: PipelineContext) => Promise<PipelineContext>;
77
77
  }
78
78
 
79
79
  /**
80
- * 处理器配置选项
80
+ * Processor configuration options
81
81
  */
82
82
  export interface ProcessorOptions {
83
- /** 是否启用调试模式 */
83
+ /** Whether to enable debug mode */
84
84
  debug?: boolean;
85
- /** 自定义日志函数 */
85
+ /** Custom logging function */
86
86
  logger?: (message: string, level?: 'info' | 'warn' | 'error') => void;
87
87
  }
88
88
 
89
89
  /**
90
- * 管道执行结果
90
+ * Pipeline execution result
91
91
  */
92
92
  export interface PipelineResult {
93
- /** 中止原因 */
93
+ /** Abort reason */
94
94
  abortReason?: string;
95
- /** 是否被中止 */
95
+ /** Whether aborted */
96
96
  isAborted: boolean;
97
- /** 最终处理的消息 */
97
+ /** Final processed messages */
98
98
  messages: any[];
99
- /** 处理过程中的元数据 */
99
+ /** Metadata from processing */
100
100
  metadata: Record<string, any>;
101
- /** 执行统计 */
101
+ /** Execution statistics */
102
102
  stats: {
103
- /** 处理的处理器数量 */
103
+ /** Number of processors processed */
104
104
  processedCount: number;
105
- /** 各处理器执行时间 */
105
+ /** Execution time for each processor */
106
106
  processorDurations: Record<string, number>;
107
- /** 总处理时间 */
107
+ /** Total processing time */
108
108
  totalDuration: number;
109
109
  };
110
110
  }
@@ -126,14 +126,14 @@ export type ProcessorTypeLegacy =
126
126
  | 'processor';
127
127
 
128
128
  /**
129
- * Token 计数器接口
129
+ * Token counter interface
130
130
  */
131
131
  export interface TokenCounter {
132
132
  count: (messages: UIChatMessage[] | string) => Promise<number>;
133
133
  }
134
134
 
135
135
  /**
136
- * 文件上下文信息
136
+ * File context information
137
137
  */
138
138
  export interface FileContext {
139
139
  addUrl?: boolean;
@@ -142,7 +142,7 @@ export interface FileContext {
142
142
  }
143
143
 
144
144
  /**
145
- * RAG 检索块
145
+ * RAG retrieval chunk
146
146
  */
147
147
  export interface RetrievalChunk {
148
148
  content: string;
@@ -152,7 +152,7 @@ export interface RetrievalChunk {
152
152
  }
153
153
 
154
154
  /**
155
- * RAG 上下文
155
+ * RAG context
156
156
  */
157
157
  export interface RAGContext {
158
158
  chunks: RetrievalChunk[];
@@ -161,7 +161,7 @@ export interface RAGContext {
161
161
  }
162
162
 
163
163
  /**
164
- * 模型能力
164
+ * Model capabilities
165
165
  */
166
166
  export interface ModelCapabilities {
167
167
  supportsFunctionCall: boolean;
@@ -171,7 +171,7 @@ export interface ModelCapabilities {
171
171
  }
172
172
 
173
173
  /**
174
- * 处理器错误
174
+ * Processor error
175
175
  */
176
176
  export class ProcessorError extends Error {
177
177
  constructor(
@@ -185,7 +185,7 @@ export class ProcessorError extends Error {
185
185
  }
186
186
 
187
187
  /**
188
- * 管道错误
188
+ * Pipeline error
189
189
  */
190
190
  export class PipelineError extends Error {
191
191
  constructor(
@@ -0,0 +1,352 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { validateRedirectHost } from './validateRedirectHost';
4
+
5
+ describe('validateRedirectHost', () => {
6
+ let originalAppUrl: string | undefined;
7
+
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+ // Store original APP_URL and set default for tests
11
+ originalAppUrl = process.env.APP_URL;
12
+ process.env.APP_URL = 'https://example.com';
13
+ });
14
+
15
+ afterEach(() => {
16
+ // Restore original APP_URL
17
+ if (originalAppUrl === undefined) {
18
+ delete process.env.APP_URL;
19
+ } else {
20
+ process.env.APP_URL = originalAppUrl;
21
+ }
22
+ });
23
+
24
+ describe('invalid inputs', () => {
25
+ it('should return false when targetHost is empty string', () => {
26
+ const result = validateRedirectHost('');
27
+ expect(result).toBe(false);
28
+ });
29
+
30
+ it('should return false when targetHost is "null" string', () => {
31
+ const result = validateRedirectHost('null');
32
+ expect(result).toBe(false);
33
+ });
34
+
35
+ it('should return false when APP_URL is not configured', () => {
36
+ delete process.env.APP_URL;
37
+ const result = validateRedirectHost('example.com');
38
+ expect(result).toBe(false);
39
+ });
40
+
41
+ it('should return false when APP_URL is malformed', () => {
42
+ process.env.APP_URL = 'not-a-valid-url';
43
+ const result = validateRedirectHost('example.com');
44
+ expect(result).toBe(false);
45
+ });
46
+ });
47
+
48
+ describe('exact host match', () => {
49
+ it('should return true when targetHost exactly matches APP_URL host', () => {
50
+ const result = validateRedirectHost('example.com');
51
+ expect(result).toBe(true);
52
+ });
53
+
54
+ it('should return true when targetHost matches APP_URL host with port', () => {
55
+ process.env.APP_URL = 'https://example.com:8080';
56
+ const result = validateRedirectHost('example.com:8080');
57
+ expect(result).toBe(true);
58
+ });
59
+
60
+ it('should return true when targetHost matches APP_URL with different protocols', () => {
61
+ process.env.APP_URL = 'http://example.com';
62
+ const result = validateRedirectHost('example.com');
63
+ expect(result).toBe(true);
64
+ });
65
+
66
+ it('should return false when targetHost port differs from APP_URL', () => {
67
+ process.env.APP_URL = 'https://example.com:8080';
68
+ const result = validateRedirectHost('example.com:9090');
69
+ expect(result).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('localhost validation', () => {
74
+ it('should allow localhost when APP_URL is localhost', () => {
75
+ process.env.APP_URL = 'http://localhost:3000';
76
+ const result = validateRedirectHost('localhost');
77
+ expect(result).toBe(true);
78
+ });
79
+
80
+ it('should allow localhost with port when APP_URL is localhost', () => {
81
+ process.env.APP_URL = 'http://localhost:3000';
82
+ const result = validateRedirectHost('localhost:8080');
83
+ expect(result).toBe(true);
84
+ });
85
+
86
+ it('should allow 127.0.0.1 when APP_URL is localhost', () => {
87
+ process.env.APP_URL = 'http://localhost:3000';
88
+ const result = validateRedirectHost('127.0.0.1');
89
+ expect(result).toBe(true);
90
+ });
91
+
92
+ it('should allow 127.0.0.1 with port when APP_URL is localhost', () => {
93
+ process.env.APP_URL = 'http://localhost:3000';
94
+ const result = validateRedirectHost('127.0.0.1:8080');
95
+ expect(result).toBe(true);
96
+ });
97
+
98
+ it('should allow 0.0.0.0 when APP_URL is localhost', () => {
99
+ process.env.APP_URL = 'http://localhost:3000';
100
+ const result = validateRedirectHost('0.0.0.0');
101
+ expect(result).toBe(true);
102
+ });
103
+
104
+ it('should allow 0.0.0.0 with port when APP_URL is localhost', () => {
105
+ process.env.APP_URL = 'http://localhost:3000';
106
+ const result = validateRedirectHost('0.0.0.0:8080');
107
+ expect(result).toBe(true);
108
+ });
109
+
110
+ it('should allow localhost when APP_URL is 127.0.0.1', () => {
111
+ process.env.APP_URL = 'http://127.0.0.1:3000';
112
+ const result = validateRedirectHost('localhost');
113
+ expect(result).toBe(true);
114
+ });
115
+
116
+ it('should allow localhost when APP_URL is 0.0.0.0', () => {
117
+ process.env.APP_URL = 'http://0.0.0.0:3000';
118
+ const result = validateRedirectHost('localhost');
119
+ expect(result).toBe(true);
120
+ });
121
+
122
+ it('should reject localhost when APP_URL is not a local address', () => {
123
+ process.env.APP_URL = 'https://example.com';
124
+ const result = validateRedirectHost('localhost');
125
+ expect(result).toBe(false);
126
+ });
127
+
128
+ it('should reject 127.0.0.1 when APP_URL is not a local address', () => {
129
+ process.env.APP_URL = 'https://example.com';
130
+ const result = validateRedirectHost('127.0.0.1');
131
+ expect(result).toBe(false);
132
+ });
133
+
134
+ it('should reject 0.0.0.0 when APP_URL is not a local address', () => {
135
+ process.env.APP_URL = 'https://example.com';
136
+ const result = validateRedirectHost('0.0.0.0');
137
+ expect(result).toBe(false);
138
+ });
139
+ });
140
+
141
+ describe('subdomain validation', () => {
142
+ it('should allow valid subdomain of APP_URL domain', () => {
143
+ process.env.APP_URL = 'https://example.com';
144
+ const result = validateRedirectHost('api.example.com');
145
+ expect(result).toBe(true);
146
+ });
147
+
148
+ it('should allow multi-level subdomain', () => {
149
+ process.env.APP_URL = 'https://example.com';
150
+ const result = validateRedirectHost('api.v1.example.com');
151
+ expect(result).toBe(true);
152
+ });
153
+
154
+ it('should allow subdomain with port', () => {
155
+ process.env.APP_URL = 'https://example.com';
156
+ const result = validateRedirectHost('api.example.com:8080');
157
+ expect(result).toBe(true);
158
+ });
159
+
160
+ it('should reject domain that is not a subdomain', () => {
161
+ process.env.APP_URL = 'https://example.com';
162
+ const result = validateRedirectHost('fakeexample.com');
163
+ expect(result).toBe(false);
164
+ });
165
+
166
+ it('should reject domain that contains but is not subdomain', () => {
167
+ process.env.APP_URL = 'https://example.com';
168
+ const result = validateRedirectHost('notexample.com');
169
+ expect(result).toBe(false);
170
+ });
171
+
172
+ it('should reject completely different domain', () => {
173
+ process.env.APP_URL = 'https://example.com';
174
+ const result = validateRedirectHost('evil.com');
175
+ expect(result).toBe(false);
176
+ });
177
+
178
+ it('should handle APP_URL with port when validating subdomains', () => {
179
+ process.env.APP_URL = 'https://example.com:8080';
180
+ const result = validateRedirectHost('api.example.com');
181
+ expect(result).toBe(true);
182
+ });
183
+
184
+ it('should handle APP_URL with subdomain when validating further subdomains', () => {
185
+ process.env.APP_URL = 'https://api.example.com';
186
+ const result = validateRedirectHost('v1.api.example.com');
187
+ expect(result).toBe(true);
188
+ });
189
+ });
190
+
191
+ describe('open redirect attack prevention', () => {
192
+ it('should block redirection to malicious external domain', () => {
193
+ process.env.APP_URL = 'https://example.com';
194
+ const result = validateRedirectHost('malicious.com');
195
+ expect(result).toBe(false);
196
+ });
197
+
198
+ it('should block redirection to similar-looking domain', () => {
199
+ process.env.APP_URL = 'https://example.com';
200
+ const result = validateRedirectHost('example.com.evil.com');
201
+ expect(result).toBe(false);
202
+ });
203
+
204
+ it('should block redirection to domain with extra TLD', () => {
205
+ process.env.APP_URL = 'https://example.com';
206
+ const result = validateRedirectHost('example.com.br');
207
+ expect(result).toBe(false);
208
+ });
209
+
210
+ it('should block redirection using homograph attack attempt', () => {
211
+ process.env.APP_URL = 'https://example.com';
212
+ // Using similar-looking characters
213
+ const result = validateRedirectHost('examp1e.com');
214
+ expect(result).toBe(false);
215
+ });
216
+ });
217
+
218
+ describe('port handling', () => {
219
+ it('should handle standard HTTPS port (443) - normalized by URL API', () => {
220
+ // Note: URL API normalizes standard ports, so :443 is removed from https URLs
221
+ process.env.APP_URL = 'https://example.com:443';
222
+ // APP_URL becomes https://example.com (443 is default for https)
223
+ const result = validateRedirectHost('example.com');
224
+ expect(result).toBe(true);
225
+ });
226
+
227
+ it('should handle standard HTTP port (80) - normalized by URL API', () => {
228
+ // Note: URL API normalizes standard ports, so :80 is removed from http URLs
229
+ process.env.APP_URL = 'http://example.com:80';
230
+ // APP_URL becomes http://example.com (80 is default for http)
231
+ const result = validateRedirectHost('example.com');
232
+ expect(result).toBe(true);
233
+ });
234
+
235
+ it('should handle custom ports', () => {
236
+ process.env.APP_URL = 'https://example.com:3000';
237
+ const result = validateRedirectHost('example.com:3000');
238
+ expect(result).toBe(true);
239
+ });
240
+
241
+ it('should reject different ports on same domain', () => {
242
+ process.env.APP_URL = 'https://example.com:3000';
243
+ const result = validateRedirectHost('example.com:4000');
244
+ expect(result).toBe(false);
245
+ });
246
+
247
+ it('should allow subdomain with different port than APP_URL', () => {
248
+ process.env.APP_URL = 'https://example.com:3000';
249
+ const result = validateRedirectHost('api.example.com:8080');
250
+ expect(result).toBe(true);
251
+ });
252
+ });
253
+
254
+ describe('edge cases', () => {
255
+ it('should handle APP_URL with trailing slash', () => {
256
+ process.env.APP_URL = 'https://example.com/';
257
+ const result = validateRedirectHost('example.com');
258
+ expect(result).toBe(true);
259
+ });
260
+
261
+ it('should handle APP_URL with path', () => {
262
+ process.env.APP_URL = 'https://example.com/app';
263
+ const result = validateRedirectHost('example.com');
264
+ expect(result).toBe(true);
265
+ });
266
+
267
+ it('should handle uppercase in targetHost', () => {
268
+ process.env.APP_URL = 'https://example.com';
269
+ const result = validateRedirectHost('EXAMPLE.COM');
270
+ expect(result).toBe(false);
271
+ });
272
+
273
+ it('should handle mixed case domains - URL API lowercases hostnames', () => {
274
+ // Note: URL API automatically lowercases hostnames
275
+ process.env.APP_URL = 'https://Example.Com';
276
+ // URL API converts it to example.com
277
+ const result = validateRedirectHost('example.com');
278
+ expect(result).toBe(true);
279
+ });
280
+
281
+ it('should handle IPv4 addresses in APP_URL', () => {
282
+ process.env.APP_URL = 'http://192.168.1.1:3000';
283
+ const result = validateRedirectHost('192.168.1.1:3000');
284
+ expect(result).toBe(true);
285
+ });
286
+
287
+ it('should reject different IPv4 addresses', () => {
288
+ process.env.APP_URL = 'http://192.168.1.1:3000';
289
+ const result = validateRedirectHost('192.168.1.2:3000');
290
+ expect(result).toBe(false);
291
+ });
292
+
293
+ it('should handle empty APP_URL gracefully', () => {
294
+ process.env.APP_URL = '';
295
+ const result = validateRedirectHost('example.com');
296
+ expect(result).toBe(false);
297
+ });
298
+
299
+ it('should handle whitespace in targetHost', () => {
300
+ const result = validateRedirectHost(' example.com ');
301
+ expect(result).toBe(false);
302
+ });
303
+
304
+ it('should handle single dot in targetHost', () => {
305
+ const result = validateRedirectHost('.');
306
+ expect(result).toBe(false);
307
+ });
308
+
309
+ it('should handle double dots in targetHost', () => {
310
+ const result = validateRedirectHost('example..com');
311
+ expect(result).toBe(false);
312
+ });
313
+ });
314
+
315
+ describe('real-world scenarios', () => {
316
+ it('should validate production domain correctly', () => {
317
+ process.env.APP_URL = 'https://chat.lobehub.com';
318
+ const result = validateRedirectHost('chat.lobehub.com');
319
+ expect(result).toBe(true);
320
+ });
321
+
322
+ it('should allow API subdomain in production', () => {
323
+ process.env.APP_URL = 'https://chat.lobehub.com';
324
+ const result = validateRedirectHost('api.chat.lobehub.com');
325
+ expect(result).toBe(true);
326
+ });
327
+
328
+ it('should block redirect to competitor domain', () => {
329
+ process.env.APP_URL = 'https://chat.lobehub.com';
330
+ const result = validateRedirectHost('competitor.com');
331
+ expect(result).toBe(false);
332
+ });
333
+
334
+ it('should support development environment with port', () => {
335
+ process.env.APP_URL = 'http://localhost:3010';
336
+ const result = validateRedirectHost('localhost:3010');
337
+ expect(result).toBe(true);
338
+ });
339
+
340
+ it('should support staging environment', () => {
341
+ process.env.APP_URL = 'https://staging.example.com';
342
+ const result = validateRedirectHost('staging.example.com');
343
+ expect(result).toBe(true);
344
+ });
345
+
346
+ it('should allow preview deployment subdomain', () => {
347
+ process.env.APP_URL = 'https://example.com';
348
+ const result = validateRedirectHost('pr-123.example.com');
349
+ expect(result).toBe(true);
350
+ });
351
+ });
352
+ });
@@ -42,11 +42,9 @@ const useStyles = createStyles(({ css, token }) => ({
42
42
  background-color: transparent;
43
43
  `,
44
44
  card: css`
45
- width: 100%;
46
45
  max-width: 500px;
47
46
  border-color: ${token.colorBorderSecondary};
48
47
  border-radius: 12px;
49
-
50
48
  background-color: ${token.colorBgContainer};
51
49
  `,
52
50
  connector: css`
@@ -29,11 +29,9 @@ const useStyles = createStyles(({ css, token, responsive }) => ({
29
29
  font-weight: 500;
30
30
  `,
31
31
  card: css`
32
- width: 100%;
33
32
  max-width: 500px;
34
33
  border-color: ${token.colorBorderSecondary};
35
34
  border-radius: 12px;
36
-
37
35
  background: ${token.colorBgContainer};
38
36
 
39
37
  ${responsive.mobile} {