@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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/context-engine/src/base/BaseProcessor.ts +13 -13
- package/packages/context-engine/src/base/BaseProvider.ts +2 -2
- package/packages/context-engine/src/base/__tests__/BaseProcessor.test.ts +5 -5
- package/packages/context-engine/src/processors/MessageCleanup.ts +6 -6
- package/packages/context-engine/src/processors/MessageContent.ts +17 -17
- package/packages/context-engine/src/processors/PlaceholderVariables.ts +4 -4
- package/packages/context-engine/src/processors/ToolCall.ts +18 -18
- package/packages/context-engine/src/processors/ToolMessageReorder.ts +10 -10
- package/packages/context-engine/src/providers/HistorySummary.ts +6 -6
- package/packages/context-engine/src/providers/ToolSystemRole.ts +7 -7
- package/packages/context-engine/src/tools/ToolsEngine.ts +8 -8
- package/packages/context-engine/src/types.ts +35 -35
- package/packages/utils/src/server/validateRedirectHost.test.ts +352 -0
- package/src/app/[variants]/oauth/consent/[uid]/Consent/index.tsx +0 -2
- package/src/app/[variants]/oauth/consent/[uid]/Login.tsx +0 -2
- package/src/server/services/oidc/index.test.ts +232 -0
- package/src/server/services/oidc/index.ts +24 -0
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
60
|
+
/** Current token count estimate */
|
|
61
61
|
currentTokenCount?: number;
|
|
62
|
-
/**
|
|
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} {
|