@lobehub/lobehub 2.0.0-next.359 → 2.0.0-next.360

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 CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.360](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.359...v2.0.0-next.360)
6
+
7
+ <sup>Released on **2026-01-24**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Login success callback url error.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Login success callback url error, closes [#11763](https://github.com/lobehub/lobe-chat/issues/11763) ([f73435d](https://github.com/lobehub/lobe-chat/commit/f73435d))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.359](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.358...v2.0.0-next.359)
6
31
 
7
32
  <sup>Released on **2026-01-24**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Login success callback url error."
6
+ ]
7
+ },
8
+ "date": "2026-01-24",
9
+ "version": "2.0.0-next.360"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.359",
3
+ "version": "2.0.0-next.360",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -1,7 +1,5 @@
1
1
  export * from './auth';
2
- export * from './correctOIDCUrl';
3
2
  export * from './response';
4
3
  export * from './responsive';
5
4
  export * from './sse';
6
- export * from './validateRedirectHost';
7
5
  export * from './xor';
@@ -3,22 +3,33 @@ import { type NextRequest, NextResponse, after } from 'next/server';
3
3
 
4
4
  import { OAuthHandoffModel } from '@/database/models/oauthHandoff';
5
5
  import { serverDB } from '@/database/server';
6
- import { correctOIDCUrl } from '@/utils/server/correctOIDCUrl';
6
+ import { appEnv } from '@/envs/app';
7
7
 
8
8
  const log = debug('lobe-oidc:callback:desktop');
9
9
 
10
10
  const errorPathname = '/oauth/callback/error';
11
11
 
12
12
  /**
13
- * 安全地构建重定向URL,使用经过验证的 correctOIDCUrl 防止开放重定向攻击
13
+ * 安全地构建重定向URL - 直接使用 APP_URL 作为目标
14
14
  */
15
15
  const buildRedirectUrl = (req: NextRequest, pathname: string): URL => {
16
- // 使用 req.nextUrl 作为基础URL,然后通过 correctOIDCUrl 进行验证和修正
17
- const baseUrl = req.nextUrl.clone();
18
- baseUrl.pathname = pathname;
16
+ // 使用统一的环境变量管理
17
+ if (appEnv.APP_URL) {
18
+ try {
19
+ const baseUrl = new URL(appEnv.APP_URL);
20
+ baseUrl.pathname = pathname;
21
+ log('Using APP_URL for redirect: %s', baseUrl.toString());
22
+ return baseUrl;
23
+ } catch (error) {
24
+ log('Error parsing APP_URL, using fallback: %O', error);
25
+ }
26
+ }
19
27
 
20
- // correctOIDCUrl 会验证 X-Forwarded-* 头部并防止开放重定向攻击
21
- return correctOIDCUrl(req, baseUrl);
28
+ // 后备方案:使用 req.nextUrl
29
+ log('Warning: APP_URL not configured, using req.nextUrl as fallback');
30
+ const fallbackUrl = req.nextUrl.clone();
31
+ fallbackUrl.pathname = pathname;
32
+ return fallbackUrl;
22
33
  };
23
34
 
24
35
  export const GET = async (req: NextRequest) => {
@@ -1,7 +1,8 @@
1
- import { correctOIDCUrl, getUserAuth } from '@lobechat/utils/server';
1
+ import { getUserAuth } from '@lobechat/utils/server';
2
2
  import debug from 'debug';
3
3
  import { type NextRequest, NextResponse } from 'next/server';
4
4
 
5
+ import { appEnv } from '@/envs/app';
5
6
  import { OIDCService } from '@/server/services/oidc';
6
7
 
7
8
  const log = debug('lobe-oidc:consent');
@@ -113,15 +114,23 @@ export async function POST(request: NextRequest) {
113
114
  const internalRedirectUrlString = await oidcService.getInteractionResult(uid, result);
114
115
  log('OIDC Provider internal redirect URL string: %s', internalRedirectUrlString);
115
116
 
116
- let finalRedirectUrl;
117
- try {
118
- finalRedirectUrl = correctOIDCUrl(request, new URL(internalRedirectUrlString));
119
- } catch {
120
- finalRedirectUrl = new URL(internalRedirectUrlString);
121
- log('Warning: Could not parse redirect URL, using as-is: %s', internalRedirectUrlString);
117
+ // 直接使用 APP_URL 作为基础
118
+ if (appEnv.APP_URL) {
119
+ const baseUrl = new URL(appEnv.APP_URL);
120
+ const internalUrl = new URL(internalRedirectUrlString);
121
+ baseUrl.pathname = internalUrl.pathname;
122
+ baseUrl.search = internalUrl.search;
123
+ baseUrl.hash = internalUrl.hash;
124
+ const finalRedirectUrl = baseUrl;
125
+ log('Using APP_URL as base for redirect: %s', finalRedirectUrl.toString());
126
+ return NextResponse.redirect(finalRedirectUrl, {
127
+ status: 303,
128
+ });
122
129
  }
123
130
 
124
- return NextResponse.redirect(finalRedirectUrl, {
131
+ // 后备方案:使用原始内部URL
132
+ log('Using internal redirect URL directly: %s', internalRedirectUrlString);
133
+ return NextResponse.redirect(new URL(internalRedirectUrlString), {
125
134
  status: 303,
126
135
  });
127
136
  } catch (error) {
@@ -1,527 +0,0 @@
1
- import { NextRequest } from 'next/server';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import { correctOIDCUrl } from './correctOIDCUrl';
5
-
6
- describe('correctOIDCUrl', () => {
7
- let mockRequest: NextRequest;
8
- let originalAppUrl: string | undefined;
9
-
10
- beforeEach(() => {
11
- vi.clearAllMocks();
12
- // Store original APP_URL and set default for tests
13
- originalAppUrl = process.env.APP_URL;
14
- process.env.APP_URL = 'https://example.com';
15
-
16
- // Create a mock request with a mutable headers property
17
- mockRequest = {
18
- headers: {
19
- get: vi.fn(),
20
- },
21
- } as unknown as NextRequest;
22
- });
23
-
24
- afterEach(() => {
25
- // Restore original APP_URL
26
- if (originalAppUrl === undefined) {
27
- delete process.env.APP_URL;
28
- } else {
29
- process.env.APP_URL = originalAppUrl;
30
- }
31
- });
32
-
33
- describe('when no forwarded headers are present', () => {
34
- it('should return original URL when host matches and protocol is correct', () => {
35
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
36
- if (header === 'host') return 'example.com';
37
- return null;
38
- });
39
-
40
- const originalUrl = new URL('https://example.com/auth/callback');
41
- const result = correctOIDCUrl(mockRequest, originalUrl);
42
-
43
- expect(result.toString()).toBe('https://example.com/auth/callback');
44
- expect(result).toBe(originalUrl); // Should return the same object
45
- });
46
-
47
- it('should correct localhost URLs to request host preserving port', () => {
48
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
49
- if (header === 'host') return 'example.com';
50
- return null;
51
- });
52
-
53
- const originalUrl = new URL('http://localhost:3000/auth/callback');
54
- const result = correctOIDCUrl(mockRequest, originalUrl);
55
-
56
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
57
- expect(result.host).toBe('example.com:3000');
58
- expect(result.hostname).toBe('example.com');
59
- expect(result.port).toBe('3000');
60
- expect(result.protocol).toBe('http:');
61
- });
62
-
63
- it('should correct 127.0.0.1 URLs to request host preserving port', () => {
64
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
65
- if (header === 'host') return 'example.com';
66
- return null;
67
- });
68
-
69
- const originalUrl = new URL('http://127.0.0.1:3000/auth/callback');
70
- const result = correctOIDCUrl(mockRequest, originalUrl);
71
-
72
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
73
- expect(result.host).toBe('example.com:3000');
74
- expect(result.hostname).toBe('example.com');
75
- });
76
-
77
- it('should correct 0.0.0.0 URLs to request host preserving port', () => {
78
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
79
- if (header === 'host') return 'example.com';
80
- return null;
81
- });
82
-
83
- const originalUrl = new URL('http://0.0.0.0:3000/auth/callback');
84
- const result = correctOIDCUrl(mockRequest, originalUrl);
85
-
86
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
87
- expect(result.host).toBe('example.com:3000');
88
- expect(result.hostname).toBe('example.com');
89
- });
90
-
91
- it('should correct mismatched hostnames', () => {
92
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
93
- if (header === 'host') return 'example.com';
94
- return null;
95
- });
96
-
97
- const originalUrl = new URL('https://different.com/auth/callback');
98
- const result = correctOIDCUrl(mockRequest, originalUrl);
99
-
100
- expect(result.toString()).toBe('https://example.com/auth/callback');
101
- expect(result.host).toBe('example.com');
102
- });
103
-
104
- it('should handle request host with port when correcting localhost', () => {
105
- process.env.APP_URL = 'https://example.com:8080';
106
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
107
- if (header === 'host') return 'example.com:8080';
108
- return null;
109
- });
110
-
111
- const originalUrl = new URL('http://localhost:3000/auth/callback');
112
- const result = correctOIDCUrl(mockRequest, originalUrl);
113
-
114
- expect(result.toString()).toBe('http://example.com:8080/auth/callback');
115
- expect(result.host).toBe('example.com:8080');
116
- expect(result.hostname).toBe('example.com');
117
- expect(result.port).toBe('8080');
118
- });
119
- });
120
-
121
- describe('when x-forwarded-host header is present', () => {
122
- it('should use x-forwarded-host over host header', () => {
123
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
124
- if (header === 'host') return 'internal.com';
125
- if (header === 'x-forwarded-host') return 'proxy.example.com';
126
- return null;
127
- });
128
-
129
- const originalUrl = new URL('http://localhost:3000/auth/callback');
130
- const result = correctOIDCUrl(mockRequest, originalUrl);
131
-
132
- expect(result.toString()).toBe('http://proxy.example.com:3000/auth/callback');
133
- expect(result.host).toBe('proxy.example.com:3000');
134
- expect(result.hostname).toBe('proxy.example.com');
135
- });
136
-
137
- it('should preserve path and query parameters', () => {
138
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
139
- if (header === 'host') return 'internal.com';
140
- if (header === 'x-forwarded-host') return 'proxy.example.com';
141
- return null;
142
- });
143
-
144
- const originalUrl = new URL('http://localhost:3000/auth/callback?code=123&state=abc');
145
- const result = correctOIDCUrl(mockRequest, originalUrl);
146
-
147
- expect(result.toString()).toBe(
148
- 'http://proxy.example.com:3000/auth/callback?code=123&state=abc',
149
- );
150
- expect(result.pathname).toBe('/auth/callback');
151
- expect(result.search).toBe('?code=123&state=abc');
152
- });
153
- });
154
-
155
- describe('when x-forwarded-proto header is present', () => {
156
- it('should use x-forwarded-proto for protocol', () => {
157
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
158
- if (header === 'host') return 'example.com';
159
- if (header === 'x-forwarded-proto') return 'https';
160
- return null;
161
- });
162
-
163
- const originalUrl = new URL('http://localhost:3000/auth/callback');
164
- const result = correctOIDCUrl(mockRequest, originalUrl);
165
-
166
- expect(result.toString()).toBe('https://example.com:3000/auth/callback');
167
- expect(result.protocol).toBe('https:');
168
- });
169
-
170
- it('should use x-forwarded-protocol as fallback', () => {
171
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
172
- if (header === 'host') return 'example.com';
173
- if (header === 'x-forwarded-protocol') return 'https';
174
- return null;
175
- });
176
-
177
- const originalUrl = new URL('http://localhost:3000/auth/callback');
178
- const result = correctOIDCUrl(mockRequest, originalUrl);
179
-
180
- expect(result.toString()).toBe('https://example.com:3000/auth/callback');
181
- expect(result.protocol).toBe('https:');
182
- });
183
-
184
- it('should prioritize x-forwarded-proto over x-forwarded-protocol', () => {
185
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
186
- if (header === 'host') return 'example.com';
187
- if (header === 'x-forwarded-proto') return 'https';
188
- if (header === 'x-forwarded-protocol') return 'http';
189
- return null;
190
- });
191
-
192
- const originalUrl = new URL('http://localhost:3000/auth/callback');
193
- const result = correctOIDCUrl(mockRequest, originalUrl);
194
-
195
- expect(result.toString()).toBe('https://example.com:3000/auth/callback');
196
- expect(result.protocol).toBe('https:');
197
- });
198
- });
199
-
200
- describe('protocol inference when no forwarded protocol', () => {
201
- it('should infer https when original URL uses https', () => {
202
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
203
- if (header === 'host') return 'example.com';
204
- return null;
205
- });
206
-
207
- const originalUrl = new URL('https://localhost:3000/auth/callback');
208
- const result = correctOIDCUrl(mockRequest, originalUrl);
209
-
210
- expect(result.toString()).toBe('https://example.com:3000/auth/callback');
211
- expect(result.protocol).toBe('https:');
212
- });
213
-
214
- it('should default to http when original URL uses http', () => {
215
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
216
- if (header === 'host') return 'example.com';
217
- return null;
218
- });
219
-
220
- const originalUrl = new URL('http://localhost:3000/auth/callback');
221
- const result = correctOIDCUrl(mockRequest, originalUrl);
222
-
223
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
224
- expect(result.protocol).toBe('http:');
225
- });
226
- });
227
-
228
- describe('edge cases', () => {
229
- it('should return original URL when host is null', () => {
230
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
231
- if (header === 'host') return null;
232
- return null;
233
- });
234
-
235
- const originalUrl = new URL('http://localhost:3000/auth/callback');
236
- const result = correctOIDCUrl(mockRequest, originalUrl);
237
-
238
- expect(result).toBe(originalUrl);
239
- expect(result.toString()).toBe('http://localhost:3000/auth/callback');
240
- });
241
-
242
- it('should return original URL when host is "null" string', () => {
243
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
244
- if (header === 'host') return 'null';
245
- return null;
246
- });
247
-
248
- const originalUrl = new URL('http://localhost:3000/auth/callback');
249
- const result = correctOIDCUrl(mockRequest, originalUrl);
250
-
251
- expect(result).toBe(originalUrl);
252
- expect(result.toString()).toBe('http://localhost:3000/auth/callback');
253
- });
254
-
255
- it('should return original URL when no host header is present', () => {
256
- (mockRequest.headers.get as any).mockImplementation(() => null);
257
-
258
- const originalUrl = new URL('http://localhost:3000/auth/callback');
259
- const result = correctOIDCUrl(mockRequest, originalUrl);
260
-
261
- expect(result).toBe(originalUrl);
262
- expect(result.toString()).toBe('http://localhost:3000/auth/callback');
263
- });
264
-
265
- it('should handle URL construction errors gracefully', () => {
266
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
267
- if (header === 'host') return 'example.com';
268
- return null;
269
- });
270
-
271
- const originalUrl = new URL(
272
- 'http://localhost:3000/auth/callback?redirect=http://example.com',
273
- );
274
-
275
- // Spy on URL constructor to simulate an error on correction
276
- const urlSpy = vi.spyOn(global, 'URL');
277
- urlSpy.mockImplementationOnce((url: string | URL, base?: string | URL) => new URL(url, base)); // First call succeeds (original)
278
- urlSpy.mockImplementationOnce(() => {
279
- throw new Error('Invalid URL');
280
- }); // Second call fails (correction)
281
-
282
- const result = correctOIDCUrl(mockRequest, originalUrl);
283
-
284
- // Should return original URL when correction fails
285
- expect(result).toBe(originalUrl);
286
- expect(result.toString()).toBe(
287
- 'http://localhost:3000/auth/callback?redirect=http://example.com',
288
- );
289
-
290
- urlSpy.mockRestore();
291
- });
292
- });
293
-
294
- describe('complex scenarios', () => {
295
- it('should handle complete proxy scenario with all headers', () => {
296
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
297
- if (header === 'host') return 'internal-service:3000';
298
- if (header === 'x-forwarded-host') return 'api.example.com';
299
- if (header === 'x-forwarded-proto') return 'https';
300
- return null;
301
- });
302
-
303
- const originalUrl = new URL('http://localhost:8080/api/auth/callback?code=xyz&state=def');
304
- const result = correctOIDCUrl(mockRequest, originalUrl);
305
-
306
- expect(result.toString()).toBe(
307
- 'https://api.example.com:8080/api/auth/callback?code=xyz&state=def',
308
- );
309
- expect(result.protocol).toBe('https:');
310
- expect(result.host).toBe('api.example.com:8080');
311
- expect(result.hostname).toBe('api.example.com');
312
- expect(result.pathname).toBe('/api/auth/callback');
313
- expect(result.search).toBe('?code=xyz&state=def');
314
- });
315
-
316
- it('should preserve URL hash fragments', () => {
317
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
318
- if (header === 'host') return 'example.com';
319
- if (header === 'x-forwarded-proto') return 'https';
320
- return null;
321
- });
322
-
323
- const originalUrl = new URL('http://localhost:3000/auth/callback#access_token=123');
324
- const result = correctOIDCUrl(mockRequest, originalUrl);
325
-
326
- expect(result.toString()).toBe('https://example.com:3000/auth/callback#access_token=123');
327
- expect(result.hash).toBe('#access_token=123');
328
- });
329
-
330
- it('should reject forwarded host with non-standard port for security', () => {
331
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
332
- if (header === 'host') return 'internal.com:3000';
333
- if (header === 'x-forwarded-host') return 'example.com:8443';
334
- if (header === 'x-forwarded-proto') return 'https';
335
- return null;
336
- });
337
-
338
- const originalUrl = new URL('http://localhost:3000/auth/callback');
339
- const result = correctOIDCUrl(mockRequest, originalUrl);
340
-
341
- // Should fall back to host header because example.com:8443 doesn't match configured APP_URL
342
- expect(result.toString()).toBe('https://internal.com:3000/auth/callback');
343
- });
344
-
345
- it('should not need correction when URL hostname matches actual host', () => {
346
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
347
- if (header === 'host') return 'example.com';
348
- return null;
349
- });
350
-
351
- const originalUrl = new URL('http://example.com/auth/callback');
352
- const result = correctOIDCUrl(mockRequest, originalUrl);
353
-
354
- expect(result).toBe(originalUrl); // Should return the same object
355
- expect(result.toString()).toBe('http://example.com/auth/callback');
356
- });
357
- });
358
-
359
- describe('Open Redirect protection', () => {
360
- it('should prevent redirection to malicious external domains via x-forwarded-host', () => {
361
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
362
- if (header === 'host') return 'example.com';
363
- if (header === 'x-forwarded-host') return 'malicious.com';
364
- return null;
365
- });
366
-
367
- const originalUrl = new URL('http://localhost:3000/auth/callback');
368
- const result = correctOIDCUrl(mockRequest, originalUrl);
369
-
370
- // Should fall back to host header and not redirect to malicious.com
371
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
372
- });
373
-
374
- it('should allow redirection to configured domain (example.com)', () => {
375
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
376
- if (header === 'host') return 'example.com';
377
- return null;
378
- });
379
-
380
- const originalUrl = new URL('http://localhost:3000/auth/callback');
381
- const result = correctOIDCUrl(mockRequest, originalUrl);
382
-
383
- // Should allow correction to example.com (configured in APP_URL)
384
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
385
- expect(result.host).toBe('example.com:3000');
386
- });
387
-
388
- it('should allow redirection to subdomains of configured domain', () => {
389
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
390
- if (header === 'host') return 'api.example.com';
391
- return null;
392
- });
393
-
394
- const originalUrl = new URL('http://localhost:3000/auth/callback');
395
- const result = correctOIDCUrl(mockRequest, originalUrl);
396
-
397
- // Should allow correction to subdomain of example.com
398
- expect(result.toString()).toBe('http://api.example.com:3000/auth/callback');
399
- expect(result.host).toBe('api.example.com:3000');
400
- });
401
-
402
- it('should prevent redirection via x-forwarded-host to malicious domains', () => {
403
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
404
- if (header === 'host') return 'example.com'; // Trusted internal host
405
- if (header === 'x-forwarded-host') return 'evil.com'; // Malicious forwarded host
406
- return null;
407
- });
408
-
409
- const originalUrl = new URL('http://localhost:3000/auth/callback');
410
- const result = correctOIDCUrl(mockRequest, originalUrl);
411
-
412
- // Should fall back to request host (example.com) and not redirect to evil.com
413
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
414
- expect(result.hostname).not.toBe('evil.com');
415
- });
416
-
417
- it('should allow localhost in development environment', () => {
418
- // Set APP_URL to localhost for development testing
419
- process.env.APP_URL = 'http://localhost:3000';
420
-
421
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
422
- if (header === 'host') return 'localhost:8080';
423
- return null;
424
- });
425
-
426
- const originalUrl = new URL('http://127.0.0.1:3000/auth/callback');
427
- const result = correctOIDCUrl(mockRequest, originalUrl);
428
-
429
- // Should allow correction to localhost in dev environment
430
- expect(result.toString()).toBe('http://localhost:8080/auth/callback');
431
- expect(result.host).toBe('localhost:8080');
432
- });
433
-
434
- it('should prevent redirection when APP_URL is not configured', () => {
435
- // Remove APP_URL to simulate missing configuration
436
- delete process.env.APP_URL;
437
-
438
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
439
- if (header === 'host') return 'example.com';
440
- if (header === 'x-forwarded-host') return 'any-domain.com';
441
- return null;
442
- });
443
-
444
- const originalUrl = new URL('http://localhost:3000/auth/callback');
445
- const result = correctOIDCUrl(mockRequest, originalUrl);
446
-
447
- // Should fall back to host header when APP_URL is not configured and forwarded host is present
448
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
449
- });
450
-
451
- it('should handle domains that look like subdomains but are not', () => {
452
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
453
- if (header === 'host') return 'example.com';
454
- if (header === 'x-forwarded-host') return 'fakeexample.com'; // Not a subdomain of example.com
455
- return null;
456
- });
457
-
458
- const originalUrl = new URL('http://localhost:3000/auth/callback');
459
- const result = correctOIDCUrl(mockRequest, originalUrl);
460
-
461
- // Should fall back to host header
462
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
463
- });
464
-
465
- it('should reject invalid forwarded protocol', () => {
466
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
467
- if (header === 'host') return 'example.com';
468
- if (header === 'x-forwarded-host') return 'example.com';
469
- if (header === 'x-forwarded-proto') return 'javascript'; // Invalid protocol
470
- return null;
471
- });
472
-
473
- const originalUrl = new URL('http://localhost:3000/auth/callback');
474
- const result = correctOIDCUrl(mockRequest, originalUrl);
475
-
476
- // Should fall back to http protocol from URL
477
- expect(result.protocol).toBe('http:');
478
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
479
- });
480
-
481
- it('should handle uppercase in forwarded protocol', () => {
482
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
483
- if (header === 'host') return 'example.com';
484
- if (header === 'x-forwarded-host') return 'example.com';
485
- if (header === 'x-forwarded-proto') return 'HTTPS'; // Uppercase
486
- return null;
487
- });
488
-
489
- const originalUrl = new URL('http://localhost:3000/auth/callback');
490
- const result = correctOIDCUrl(mockRequest, originalUrl);
491
-
492
- // Should normalize to lowercase
493
- expect(result.protocol).toBe('https:');
494
- expect(result.toString()).toBe('https://example.com:3000/auth/callback');
495
- });
496
-
497
- it('should handle multiple hosts in x-forwarded-host', () => {
498
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
499
- if (header === 'host') return 'internal.com';
500
- if (header === 'x-forwarded-host') return 'example.com,attacker.com'; // Multiple hosts
501
- return null;
502
- });
503
-
504
- const originalUrl = new URL('http://localhost:3000/auth/callback');
505
- const result = correctOIDCUrl(mockRequest, originalUrl);
506
-
507
- // Should use the first (leftmost) host
508
- expect(result.hostname).toBe('example.com');
509
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
510
- });
511
-
512
- it('should fall back to request host when forwarded host is invalid', () => {
513
- (mockRequest.headers.get as any).mockImplementation((header: string) => {
514
- if (header === 'host') return 'example.com';
515
- if (header === 'x-forwarded-host') return 'evil.com'; // Invalid
516
- return null;
517
- });
518
-
519
- const originalUrl = new URL('http://localhost:3000/auth/callback');
520
- const result = correctOIDCUrl(mockRequest, originalUrl);
521
-
522
- // Should fall back to request host
523
- expect(result.hostname).toBe('example.com');
524
- expect(result.toString()).toBe('http://example.com:3000/auth/callback');
525
- });
526
- });
527
- });
@@ -1,134 +0,0 @@
1
- import debug from 'debug';
2
- import { NextRequest } from 'next/server';
3
-
4
- import { validateRedirectHost } from './validateRedirectHost';
5
-
6
- const log = debug('lobe-oidc:correctOIDCUrl');
7
-
8
- // Allowed protocols for security
9
- const ALLOWED_PROTOCOLS = ['http', 'https'] as const;
10
-
11
- /**
12
- * Fix OIDC redirect URL issues in proxy environments
13
- *
14
- * This function:
15
- * 1. Validates protocol against whitelist (http, https only)
16
- * 2. Handles X-Forwarded-Host with multiple values (RFC 7239)
17
- * 3. Validates X-Forwarded-Host against APP_URL to prevent open redirect attacks
18
- * 4. Provides fallback logic for invalid forwarded values
19
- *
20
- * Note: Only X-Forwarded-Host is validated, not the Host header. This is because:
21
- * - X-Forwarded-Host can be injected by attackers
22
- * - Host header comes from the reverse proxy or direct access, which is trusted
23
- *
24
- * @param req - Next.js request object
25
- * @param url - URL object to fix
26
- * @returns Fixed URL object
27
- */
28
- export const correctOIDCUrl = (req: NextRequest, url: URL): URL => {
29
- log('Input URL: %s', url.toString());
30
-
31
- // Get request headers for origin determination
32
- const requestHost = req.headers.get('host');
33
- const forwardedHost = req.headers.get('x-forwarded-host');
34
- const forwardedProto =
35
- req.headers.get('x-forwarded-proto') || req.headers.get('x-forwarded-protocol');
36
-
37
- log(
38
- 'Getting safe origin - requestHost: %s, forwardedHost: %s, forwardedProto: %s',
39
- requestHost,
40
- forwardedHost,
41
- forwardedProto,
42
- );
43
-
44
- // Determine actual hostname with fallback values
45
- // Handle multiple hosts in X-Forwarded-Host (RFC 7239: comma-separated)
46
- let actualHost = forwardedHost || requestHost;
47
- if (forwardedHost && forwardedHost.includes(',')) {
48
- // Take the first (leftmost) host as the original client's request
49
- actualHost = forwardedHost.split(',')[0]!.trim();
50
- log('Multiple hosts in X-Forwarded-Host, using first: %s', actualHost);
51
- }
52
-
53
- // Determine actual protocol with validation
54
- // Use URL's protocol as fallback to preserve original behavior
55
- let actualProto: string | null | undefined = forwardedProto;
56
- if (actualProto) {
57
- // Validate protocol is http or https
58
- const protoLower = actualProto.toLowerCase();
59
- if (!ALLOWED_PROTOCOLS.includes(protoLower as any)) {
60
- log('Warning: Invalid protocol %s, ignoring', actualProto);
61
- actualProto = null;
62
- } else {
63
- actualProto = protoLower;
64
- }
65
- }
66
-
67
- // Fallback protocol priority: URL protocol > request.nextUrl.protocol > 'https'
68
- if (!actualProto) {
69
- actualProto = url.protocol === 'https:' ? 'https' : 'http';
70
- }
71
-
72
- // If unable to determine valid hostname, return original URL
73
- if (!actualHost || actualHost === 'null') {
74
- log('Warning: Cannot determine valid host, returning original URL');
75
- return url;
76
- }
77
-
78
- // Validate only X-Forwarded-Host for security, prevent Open Redirect attacks
79
- // Host header is trusted (comes from reverse proxy or direct access)
80
- if (forwardedHost && !validateRedirectHost(actualHost)) {
81
- log('Warning: X-Forwarded-Host %s failed validation, falling back to request host', actualHost);
82
- // Try to fall back to request host if forwarded host is invalid
83
- if (requestHost) {
84
- actualHost = requestHost;
85
- } else {
86
- // No valid host available
87
- log('Error: No valid host available after validation, returning original URL');
88
- return url;
89
- }
90
- }
91
-
92
- // Build safe origin
93
- const safeOrigin = `${actualProto}://${actualHost}`;
94
- log('Safe origin: %s', safeOrigin);
95
-
96
- // Parse safe origin to get hostname and protocol
97
- let safeOriginUrl: URL;
98
- try {
99
- safeOriginUrl = new URL(safeOrigin);
100
- } catch (error) {
101
- log('Error parsing safe origin: %O', error);
102
- return url;
103
- }
104
-
105
- // Correct URL if it points to localhost or hostname doesn't match actual request host
106
- const needsCorrection =
107
- url.hostname === 'localhost' ||
108
- url.hostname === '127.0.0.1' ||
109
- url.hostname === '0.0.0.0' ||
110
- url.hostname !== safeOriginUrl.hostname;
111
-
112
- if (!needsCorrection) {
113
- log('URL does not need correction, returning original: %s', url.toString());
114
- return url;
115
- }
116
-
117
- log(
118
- 'URL needs correction. Original hostname: %s, correcting to: %s',
119
- url.hostname,
120
- safeOriginUrl.hostname,
121
- );
122
-
123
- try {
124
- const correctedUrl = new URL(url.toString());
125
- correctedUrl.protocol = safeOriginUrl.protocol;
126
- correctedUrl.host = safeOriginUrl.host;
127
-
128
- log('Corrected URL: %s', correctedUrl.toString());
129
- return correctedUrl;
130
- } catch (error) {
131
- log('Error creating corrected URL, returning original: %O', error);
132
- return url;
133
- }
134
- };
@@ -1,352 +0,0 @@
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
- });
@@ -1,68 +0,0 @@
1
- import debug from 'debug';
2
-
3
- const log = debug('lobe-oidc:validateRedirectHost');
4
-
5
- /**
6
- * Validate if redirect host is in the allowed whitelist
7
- * Prevent Open Redirect attacks
8
- */
9
- export const validateRedirectHost = (targetHost: string): boolean => {
10
- if (!targetHost || targetHost === 'null') {
11
- log('Invalid target host: %s', targetHost);
12
- return false;
13
- }
14
-
15
- // Get configured APP_URL as base domain
16
- const appUrl = process.env.APP_URL;
17
- if (!appUrl) {
18
- log('Warning: APP_URL not configured, rejecting redirect to: %s', targetHost);
19
- return false;
20
- }
21
-
22
- try {
23
- const appUrlObj = new URL(appUrl);
24
- const appHost = appUrlObj.host;
25
-
26
- log('Validating target host: %s against app host: %s', targetHost, appHost);
27
-
28
- // Exact match
29
- if (targetHost === appHost) {
30
- log('Host validation passed: exact match');
31
- return true;
32
- }
33
-
34
- // Allow localhost and local addresses (development environment)
35
- const isLocalhost =
36
- targetHost === 'localhost' ||
37
- targetHost.startsWith('localhost:') ||
38
- targetHost === '127.0.0.1' ||
39
- targetHost.startsWith('127.0.0.1:') ||
40
- targetHost === '0.0.0.0' ||
41
- targetHost.startsWith('0.0.0.0:');
42
-
43
- if (
44
- isLocalhost &&
45
- (appHost.includes('localhost') ||
46
- appHost.includes('127.0.0.1') ||
47
- appHost.includes('0.0.0.0'))
48
- ) {
49
- log('Host validation passed: localhost environment');
50
- return true;
51
- }
52
-
53
- // Check if it's a subdomain of the configured domain
54
- const appDomain = appHost.split(':')[0]; // Remove port number
55
- const targetDomain = targetHost.split(':')[0]; // Remove port number
56
-
57
- if (targetDomain.endsWith('.' + appDomain)) {
58
- log('Host validation passed: subdomain of %s', appDomain);
59
- return true;
60
- }
61
-
62
- console.error('Host validation failed: %s is not allowed', targetHost);
63
- return false;
64
- } catch (error) {
65
- console.error('Error parsing APP_URL %s: %O', appUrl, error);
66
- return false;
67
- }
68
- };