@lobehub/lobehub 2.0.0-next.328 → 2.0.0-next.329
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/.env.example +0 -3
- package/.env.example.development +0 -3
- package/CHANGELOG.md +33 -0
- package/Dockerfile +1 -2
- package/changelog/v1.json +9 -0
- package/docs/self-hosting/advanced/auth.mdx +5 -6
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +5 -6
- package/docs/self-hosting/environment-variables/auth.mdx +0 -7
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +0 -7
- package/locales/en-US/discover.json +1 -0
- package/locales/zh-CN/discover.json +1 -0
- package/package.json +1 -1
- package/scripts/prebuild.mts +2 -2
- package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +1 -0
- package/src/envs/__tests__/app.test.ts +81 -0
- package/src/envs/app.ts +14 -2
- package/src/envs/auth.test.ts +0 -13
- package/src/envs/auth.ts +0 -41
- package/src/libs/better-auth/auth-client.ts +0 -9
- package/src/libs/better-auth/define-config.ts +13 -12
- package/src/libs/better-auth/sso/index.ts +2 -1
- package/src/libs/better-auth/utils/config.ts +2 -2
- package/src/libs/next/proxy/define-config.ts +4 -6
- package/src/locales/default/discover.ts +2 -0
- package/src/server/routers/lambda/__tests__/integration/topic.integration.test.ts +74 -0
- package/src/server/routers/lambda/topic.ts +6 -0
- package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +4 -1
- package/src/server/services/agentRuntime/AgentRuntimeService.ts +2 -1
- package/src/services/topic/index.ts +4 -0
- package/src/store/chat/slices/topic/action.test.ts +2 -1
- package/src/store/chat/slices/topic/action.ts +1 -1
package/.env.example
CHANGED
|
@@ -307,9 +307,6 @@ OPENAI_API_KEY=sk-xxxxxxxxx
|
|
|
307
307
|
# Shared between Better-Auth and Next-Auth
|
|
308
308
|
# AUTH_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
309
309
|
|
|
310
|
-
# Auth URL (accessible from browser, optional if same domain)
|
|
311
|
-
# NEXT_PUBLIC_AUTH_URL=http://localhost:3210
|
|
312
|
-
|
|
313
310
|
# Require email verification before allowing users to sign in (default: false)
|
|
314
311
|
# Set to '1' to force users to verify their email before signing in
|
|
315
312
|
# NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION=0
|
package/.env.example.development
CHANGED
|
@@ -43,9 +43,6 @@ NEXT_PUBLIC_ENABLE_BETTER_AUTH=1
|
|
|
43
43
|
# Better Auth secret for JWT signing (generate with: openssl rand -base64 32)
|
|
44
44
|
AUTH_SECRET=${UNSAFE_SECRET}
|
|
45
45
|
|
|
46
|
-
# Authentication URL
|
|
47
|
-
NEXT_PUBLIC_AUTH_URL=${APP_URL}
|
|
48
|
-
|
|
49
46
|
# SSO providers configuration - using Casdoor for development
|
|
50
47
|
AUTH_SSO_PROVIDERS=casdoor
|
|
51
48
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.329](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.328...v2.0.0-next.329)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-21**</sup>
|
|
8
|
+
|
|
9
|
+
#### ♻ Code Refactoring
|
|
10
|
+
|
|
11
|
+
- **auth**: Remove NEXT_PUBLIC_AUTH_URL env variable.
|
|
12
|
+
|
|
13
|
+
#### 🐛 Bug Fixes
|
|
14
|
+
|
|
15
|
+
- **misc**: Sloved the old removeSessionTopics not work.
|
|
16
|
+
|
|
17
|
+
<br/>
|
|
18
|
+
|
|
19
|
+
<details>
|
|
20
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
21
|
+
|
|
22
|
+
#### Code refactoring
|
|
23
|
+
|
|
24
|
+
- **auth**: Remove NEXT_PUBLIC_AUTH_URL env variable, closes [#11658](https://github.com/lobehub/lobe-chat/issues/11658) ([c0f9875](https://github.com/lobehub/lobe-chat/commit/c0f9875))
|
|
25
|
+
|
|
26
|
+
#### What's fixed
|
|
27
|
+
|
|
28
|
+
- **misc**: Sloved the old removeSessionTopics not work, closes [#11671](https://github.com/lobehub/lobe-chat/issues/11671) ([06d41e5](https://github.com/lobehub/lobe-chat/commit/06d41e5))
|
|
29
|
+
|
|
30
|
+
</details>
|
|
31
|
+
|
|
32
|
+
<div align="right">
|
|
33
|
+
|
|
34
|
+
[](#readme-top)
|
|
35
|
+
|
|
36
|
+
</div>
|
|
37
|
+
|
|
5
38
|
## [Version 2.0.0-next.328](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.327...v2.0.0-next.328)
|
|
6
39
|
|
|
7
40
|
<sup>Released on **2026-01-20**</sup>
|
package/Dockerfile
CHANGED
package/changelog/v1.json
CHANGED
|
@@ -39,12 +39,11 @@ By setting the environment variables `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CL
|
|
|
39
39
|
|
|
40
40
|
To enable Better Auth in LobeChat, set the following environment variables:
|
|
41
41
|
|
|
42
|
-
| Environment Variable | Type | Description
|
|
43
|
-
| -------------------------------- | -------- |
|
|
44
|
-
| `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | Required | Set to `1` to enable Better Auth service
|
|
45
|
-
| `AUTH_SECRET` | Required | Key used to encrypt session tokens. Generate using: `openssl rand -base64 32`
|
|
46
|
-
| `
|
|
47
|
-
| `AUTH_SSO_PROVIDERS` | Optional | Comma-separated list of enabled SSO providers, e.g., `google,github,microsoft` |
|
|
42
|
+
| Environment Variable | Type | Description |
|
|
43
|
+
| -------------------------------- | -------- | ----------------------------------------------------------------------------- |
|
|
44
|
+
| `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | Required | Set to `1` to enable Better Auth service |
|
|
45
|
+
| `AUTH_SECRET` | Required | Key used to encrypt session tokens. Generate using: `openssl rand -base64 32` |
|
|
46
|
+
| `AUTH_SSO_PROVIDERS` | Optional | Comma-separated list of enabled SSO providers, e.g., `google,github,microsoft`|
|
|
48
47
|
|
|
49
48
|
<Callout type={'error'}>
|
|
50
49
|
**Important**: Better Auth is currently only suitable for **fresh deployments**. If you are already using NextAuth or Clerk and have existing user data in your database, **do not switch to Better Auth yet**, otherwise existing users will not be able to log in.
|
|
@@ -37,12 +37,11 @@ LobeChat 与 Clerk 做了深度集成,能够为用户提供一个更加安全
|
|
|
37
37
|
|
|
38
38
|
要在 LobeChat 中启用 Better Auth,请设置以下环境变量:
|
|
39
39
|
|
|
40
|
-
| 环境变量 | 类型 | 描述
|
|
41
|
-
| -------------------------------- | -- |
|
|
42
|
-
| `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | 必选 | 设置为 `1` 以启用 Better Auth 服务
|
|
43
|
-
| `AUTH_SECRET` | 必选 | 用于加密会话令牌的密钥。使用以下命令生成:`openssl rand -base64 32`
|
|
44
|
-
| `
|
|
45
|
-
| `AUTH_SSO_PROVIDERS` | 可选 | 启用的 SSO 提供商列表,以逗号分隔,例如 `google,github,microsoft` |
|
|
40
|
+
| 环境变量 | 类型 | 描述 |
|
|
41
|
+
| -------------------------------- | -- | ----------------------------------------------------------- |
|
|
42
|
+
| `NEXT_PUBLIC_ENABLE_BETTER_AUTH` | 必选 | 设置为 `1` 以启用 Better Auth 服务 |
|
|
43
|
+
| `AUTH_SECRET` | 必选 | 用于加密会话令牌的密钥。使用以下命令生成:`openssl rand -base64 32` |
|
|
44
|
+
| `AUTH_SSO_PROVIDERS` | 可选 | 启用的 SSO 提供商列表,以逗号分隔,例如 `google,github,microsoft` |
|
|
46
45
|
|
|
47
46
|
<Callout type={'error'}>
|
|
48
47
|
**重要提示**:Better Auth 目前仅适用于**全新部署**的场景。如果你已经使用 NextAuth 或 Clerk 并且数据库中存在用户数据,**请暂时不要切换到 Better Auth**,否则现有用户将无法登录。
|
|
@@ -34,13 +34,6 @@ LobeChat provides a complete authentication service capability when deployed. Th
|
|
|
34
34
|
- Default: `-`
|
|
35
35
|
- Example: `Tfhi2t2pelSMEA8eaV61KaqPNEndFFdMIxDaJnS1CUI=`
|
|
36
36
|
|
|
37
|
-
#### `NEXT_PUBLIC_AUTH_URL`
|
|
38
|
-
|
|
39
|
-
- Type: Optional
|
|
40
|
-
- Description: The URL accessible from the browser for Better Auth callbacks. Only set this if the default generated URL is incorrect.
|
|
41
|
-
- Default: `-`
|
|
42
|
-
- Example: `https://example.com`
|
|
43
|
-
|
|
44
37
|
#### `NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION`
|
|
45
38
|
|
|
46
39
|
- Type: Optional
|
|
@@ -32,13 +32,6 @@ LobeChat 在部署时提供了完善的身份验证服务能力,以下是相
|
|
|
32
32
|
- 默认值:`-`
|
|
33
33
|
- 示例:`Tfhi2t2pelSMEA8eaV61KaqPNEndFFdMIxDaJnS1CUI=`
|
|
34
34
|
|
|
35
|
-
#### `NEXT_PUBLIC_AUTH_URL`
|
|
36
|
-
|
|
37
|
-
- 类型:可选
|
|
38
|
-
- 描述:浏览器可访问的 Better Auth 回调 URL。仅在默认生成的 URL 不正确时设置。
|
|
39
|
-
- 默认值:`-`
|
|
40
|
-
- 示例:`https://example.com`
|
|
41
|
-
|
|
42
35
|
#### `NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION`
|
|
43
36
|
|
|
44
37
|
- 类型:可选
|
|
@@ -141,6 +141,7 @@
|
|
|
141
141
|
"filterBy.timePeriod.year": "Last Year",
|
|
142
142
|
"footer.desc": "Evolve with AI users worldwide. Become a creator to submit your agents and skills to the LobeHub Community.",
|
|
143
143
|
"footer.title": "Share your creation on LobeHub Community today",
|
|
144
|
+
"groupAgents.tag": "Group",
|
|
144
145
|
"home.communityAgents": "Community Agents",
|
|
145
146
|
"home.featuredAssistants": "Featured Agents",
|
|
146
147
|
"home.featuredModels": "Featured Models",
|
|
@@ -141,6 +141,7 @@
|
|
|
141
141
|
"filterBy.timePeriod.year": "近一年",
|
|
142
142
|
"footer.desc": "与全球 AI 用户共同进化。成为创作者,向 LobeHub 社区提交你的助手和技能。",
|
|
143
143
|
"footer.title": "立即在 LobeHub 社区分享你的创作",
|
|
144
|
+
"groupAgents.tag": "群组",
|
|
144
145
|
"home.communityAgents": "社区助理",
|
|
145
146
|
"home.featuredAssistants": "推荐助理",
|
|
146
147
|
"home.featuredModels": "推荐模型",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.329",
|
|
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",
|
package/scripts/prebuild.mts
CHANGED
|
@@ -51,10 +51,10 @@ const printEnvInfo = () => {
|
|
|
51
51
|
|
|
52
52
|
// Auth-related env vars
|
|
53
53
|
console.log('\n Auth Environment Variables:');
|
|
54
|
-
console.log(` NEXT_PUBLIC_AUTH_URL: ${process.env.NEXT_PUBLIC_AUTH_URL ?? '(not set)'}`);
|
|
55
|
-
console.log(` NEXTAUTH_URL: ${process.env.NEXTAUTH_URL ?? '(not set)'}`);
|
|
56
54
|
console.log(` APP_URL: ${process.env.APP_URL ?? '(not set)'}`);
|
|
57
55
|
console.log(` VERCEL_URL: ${process.env.VERCEL_URL ?? '(not set)'}`);
|
|
56
|
+
console.log(` VERCEL_BRANCH_URL: ${process.env.VERCEL_BRANCH_URL ?? '(not set)'}`);
|
|
57
|
+
console.log(` VERCEL_PROJECT_PRODUCTION_URL: ${process.env.VERCEL_PROJECT_PRODUCTION_URL ?? '(not set)'}`);
|
|
58
58
|
console.log(` AUTH_EMAIL_VERIFICATION: ${process.env.AUTH_EMAIL_VERIFICATION ?? '(not set)'}`);
|
|
59
59
|
console.log(` ENABLE_MAGIC_LINK: ${process.env.ENABLE_MAGIC_LINK ?? '(not set)'}`);
|
|
60
60
|
console.log(` AUTH_SECRET: ${process.env.AUTH_SECRET ? '✓ set' : '✗ not set'}`);
|
|
@@ -82,3 +82,84 @@ describe('getServerConfig', () => {
|
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
describe('APP_URL fallback', () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.resetModules();
|
|
89
|
+
// Clean up all related env vars
|
|
90
|
+
delete process.env.APP_URL;
|
|
91
|
+
delete process.env.VERCEL;
|
|
92
|
+
delete process.env.VERCEL_ENV;
|
|
93
|
+
delete process.env.VERCEL_URL;
|
|
94
|
+
delete process.env.VERCEL_BRANCH_URL;
|
|
95
|
+
delete process.env.VERCEL_PROJECT_PRODUCTION_URL;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should use APP_URL when explicitly set', async () => {
|
|
99
|
+
process.env.APP_URL = 'https://custom-app.com';
|
|
100
|
+
process.env.VERCEL = '1';
|
|
101
|
+
|
|
102
|
+
const { getAppConfig } = await import('../app');
|
|
103
|
+
const config = getAppConfig();
|
|
104
|
+
expect(config.APP_URL).toBe('https://custom-app.com');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('Vercel environment', () => {
|
|
108
|
+
it('should use VERCEL_PROJECT_PRODUCTION_URL in production', async () => {
|
|
109
|
+
process.env.VERCEL = '1';
|
|
110
|
+
process.env.VERCEL_ENV = 'production';
|
|
111
|
+
process.env.VERCEL_PROJECT_PRODUCTION_URL = 'lobechat.vercel.app';
|
|
112
|
+
process.env.VERCEL_BRANCH_URL = 'lobechat-git-main-org.vercel.app';
|
|
113
|
+
process.env.VERCEL_URL = 'lobechat-abc123.vercel.app';
|
|
114
|
+
|
|
115
|
+
const { getAppConfig } = await import('../app');
|
|
116
|
+
const config = getAppConfig();
|
|
117
|
+
expect(config.APP_URL).toBe('https://lobechat.vercel.app');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should use VERCEL_BRANCH_URL in preview environment', async () => {
|
|
121
|
+
process.env.VERCEL = '1';
|
|
122
|
+
process.env.VERCEL_ENV = 'preview';
|
|
123
|
+
process.env.VERCEL_BRANCH_URL = 'lobechat-git-feature-org.vercel.app';
|
|
124
|
+
process.env.VERCEL_URL = 'lobechat-abc123.vercel.app';
|
|
125
|
+
|
|
126
|
+
const { getAppConfig } = await import('../app');
|
|
127
|
+
const config = getAppConfig();
|
|
128
|
+
expect(config.APP_URL).toBe('https://lobechat-git-feature-org.vercel.app');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should fallback to VERCEL_URL when VERCEL_BRANCH_URL is not set', async () => {
|
|
132
|
+
process.env.VERCEL = '1';
|
|
133
|
+
process.env.VERCEL_ENV = 'preview';
|
|
134
|
+
process.env.VERCEL_URL = 'lobechat-abc123.vercel.app';
|
|
135
|
+
|
|
136
|
+
const { getAppConfig } = await import('../app');
|
|
137
|
+
const config = getAppConfig();
|
|
138
|
+
expect(config.APP_URL).toBe('https://lobechat-abc123.vercel.app');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('local environment', () => {
|
|
143
|
+
it('should use localhost:3010 in development', async () => {
|
|
144
|
+
|
|
145
|
+
vi.stubEnv('NODE_ENV', 'development');
|
|
146
|
+
|
|
147
|
+
const { getAppConfig } = await import('../app');
|
|
148
|
+
const config = getAppConfig();
|
|
149
|
+
expect(config.APP_URL).toBe('http://localhost:3010');
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should use localhost:3210 in non-development', async () => {
|
|
155
|
+
|
|
156
|
+
vi.stubEnv('NODE_ENV', 'test');
|
|
157
|
+
|
|
158
|
+
const { getAppConfig } = await import('../app');
|
|
159
|
+
const config = getAppConfig();
|
|
160
|
+
expect(config.APP_URL).toBe('http://localhost:3210');
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
package/src/envs/app.ts
CHANGED
|
@@ -12,12 +12,24 @@ declare global {
|
|
|
12
12
|
}
|
|
13
13
|
const isInVercel = process.env.VERCEL === '1';
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Vercel URL fallback order (by stability):
|
|
16
|
+
// 1. VERCEL_PROJECT_PRODUCTION_URL - project level, most stable
|
|
17
|
+
// 2. VERCEL_BRANCH_URL - branch level, stable across deployments on same branch
|
|
18
|
+
// 3. VERCEL_URL - deployment level, changes every deployment
|
|
19
|
+
const getVercelUrl = () => {
|
|
20
|
+
if (process.env.VERCEL_ENV === 'production' && process.env.VERCEL_PROJECT_PRODUCTION_URL) {
|
|
21
|
+
return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
|
|
22
|
+
}
|
|
23
|
+
if (process.env.VERCEL_BRANCH_URL) {
|
|
24
|
+
return `https://${process.env.VERCEL_BRANCH_URL}`;
|
|
25
|
+
}
|
|
26
|
+
return `https://${process.env.VERCEL_URL}`;
|
|
27
|
+
};
|
|
16
28
|
|
|
17
29
|
const APP_URL = process.env.APP_URL
|
|
18
30
|
? process.env.APP_URL
|
|
19
31
|
: isInVercel
|
|
20
|
-
?
|
|
32
|
+
? getVercelUrl()
|
|
21
33
|
: process.env.NODE_ENV === 'development'
|
|
22
34
|
? 'http://localhost:3010'
|
|
23
35
|
: 'http://localhost:3210';
|
package/src/envs/auth.test.ts
CHANGED
|
@@ -44,17 +44,4 @@ describe('getAuthConfig fallbacks', () => {
|
|
|
44
44
|
|
|
45
45
|
expect(config.AUTH_SECRET).toBe('nextauth-secret');
|
|
46
46
|
});
|
|
47
|
-
|
|
48
|
-
it('should fall back to NEXTAUTH_URL origin when NEXT_PUBLIC_AUTH_URL is empty string', () => {
|
|
49
|
-
process.env.NEXT_PUBLIC_AUTH_URL = '';
|
|
50
|
-
process.env.NEXTAUTH_URL = 'https://example.com/api/auth';
|
|
51
|
-
|
|
52
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
53
|
-
// @ts-expect-error - allow overriding for test
|
|
54
|
-
globalThis.window = undefined;
|
|
55
|
-
|
|
56
|
-
const config = getAuthConfig();
|
|
57
|
-
|
|
58
|
-
expect(config.NEXT_PUBLIC_AUTH_URL).toBe('https://example.com');
|
|
59
|
-
});
|
|
60
47
|
});
|
package/src/envs/auth.ts
CHANGED
|
@@ -2,43 +2,6 @@
|
|
|
2
2
|
import { createEnv } from '@t3-oss/env-nextjs';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Resolve public auth URL with compatibility fallbacks for NextAuth and Vercel deployments.
|
|
7
|
-
*/
|
|
8
|
-
const resolvePublicAuthUrl = () => {
|
|
9
|
-
if (process.env.NEXT_PUBLIC_AUTH_URL) return process.env.NEXT_PUBLIC_AUTH_URL;
|
|
10
|
-
|
|
11
|
-
if (process.env.NEXTAUTH_URL) {
|
|
12
|
-
try {
|
|
13
|
-
return new URL(process.env.NEXTAUTH_URL).origin;
|
|
14
|
-
} catch {
|
|
15
|
-
// ignore invalid NEXTAUTH_URL
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (process.env.APP_URL) {
|
|
20
|
-
try {
|
|
21
|
-
return new URL(process.env.APP_URL).origin;
|
|
22
|
-
} catch {
|
|
23
|
-
// ignore invalid APP_URL
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (process.env.VERCEL_URL) {
|
|
28
|
-
try {
|
|
29
|
-
const normalizedVercelUrl = process.env.VERCEL_URL.startsWith('http')
|
|
30
|
-
? process.env.VERCEL_URL
|
|
31
|
-
: `https://${process.env.VERCEL_URL}`;
|
|
32
|
-
|
|
33
|
-
return new URL(normalizedVercelUrl).origin;
|
|
34
|
-
} catch {
|
|
35
|
-
// ignore invalid Vercel URL
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return undefined;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
5
|
declare global {
|
|
43
6
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
44
7
|
namespace NodeJS {
|
|
@@ -50,7 +13,6 @@ declare global {
|
|
|
50
13
|
|
|
51
14
|
// ===== Auth (shared by Better Auth / Next Auth) ===== //
|
|
52
15
|
AUTH_SECRET?: string;
|
|
53
|
-
NEXT_PUBLIC_AUTH_URL?: string;
|
|
54
16
|
AUTH_EMAIL_VERIFICATION?: string;
|
|
55
17
|
ENABLE_MAGIC_LINK?: string;
|
|
56
18
|
AUTH_SSO_PROVIDERS?: string;
|
|
@@ -180,7 +142,6 @@ export const getAuthConfig = () => {
|
|
|
180
142
|
|
|
181
143
|
// ---------------------------------- better auth ----------------------------------
|
|
182
144
|
NEXT_PUBLIC_ENABLE_BETTER_AUTH: z.boolean().optional(),
|
|
183
|
-
NEXT_PUBLIC_AUTH_URL: z.string().optional(),
|
|
184
145
|
|
|
185
146
|
// ---------------------------------- next auth ----------------------------------
|
|
186
147
|
NEXT_PUBLIC_ENABLE_NEXT_AUTH: z.boolean().optional(),
|
|
@@ -310,8 +271,6 @@ export const getAuthConfig = () => {
|
|
|
310
271
|
|
|
311
272
|
// ---------------------------------- better auth ----------------------------------
|
|
312
273
|
NEXT_PUBLIC_ENABLE_BETTER_AUTH: process.env.NEXT_PUBLIC_ENABLE_BETTER_AUTH === '1',
|
|
313
|
-
// Fallback to NEXTAUTH_URL origin or Vercel deployment domain for seamless migration from next-auth
|
|
314
|
-
NEXT_PUBLIC_AUTH_URL: resolvePublicAuthUrl(),
|
|
315
274
|
// Fallback to NEXT_PUBLIC_* for seamless migration
|
|
316
275
|
AUTH_EMAIL_VERIFICATION:
|
|
317
276
|
process.env.AUTH_EMAIL_VERIFICATION === '1' ||
|
|
@@ -7,9 +7,6 @@ import {
|
|
|
7
7
|
import { createAuthClient } from 'better-auth/react';
|
|
8
8
|
|
|
9
9
|
import type { auth } from '@/auth';
|
|
10
|
-
import { getAuthConfig } from '@/envs/auth';
|
|
11
|
-
|
|
12
|
-
const { NEXT_PUBLIC_AUTH_URL } = getAuthConfig();
|
|
13
10
|
|
|
14
11
|
export const {
|
|
15
12
|
linkSocial,
|
|
@@ -24,12 +21,6 @@ export const {
|
|
|
24
21
|
unlinkAccount,
|
|
25
22
|
useSession,
|
|
26
23
|
} = createAuthClient({
|
|
27
|
-
/** The base URL of the server (optional if you're using the same domain) */
|
|
28
|
-
...(NEXT_PUBLIC_AUTH_URL
|
|
29
|
-
? {
|
|
30
|
-
baseURL: NEXT_PUBLIC_AUTH_URL,
|
|
31
|
-
}
|
|
32
|
-
: {}),
|
|
33
24
|
plugins: [
|
|
34
25
|
adminClient(),
|
|
35
26
|
inferAdditionalFields<typeof auth>(),
|
|
@@ -14,6 +14,7 @@ import { admin, emailOTP, genericOAuth, magicLink } from 'better-auth/plugins';
|
|
|
14
14
|
import { type BetterAuthPlugin } from 'better-auth/types';
|
|
15
15
|
|
|
16
16
|
import { businessEmailValidator } from '@/business/server/better-auth';
|
|
17
|
+
import { appEnv } from '@/envs/app';
|
|
17
18
|
import { authEnv } from '@/envs/auth';
|
|
18
19
|
import {
|
|
19
20
|
getMagicLinkEmailTemplate,
|
|
@@ -32,13 +33,13 @@ import { UserService } from '@/server/services/user';
|
|
|
32
33
|
const VERIFICATION_LINK_EXPIRES_IN = 3600;
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
* Safely extract hostname from
|
|
36
|
-
* Returns undefined if
|
|
36
|
+
* Safely extract hostname from APP_URL for passkey rpID.
|
|
37
|
+
* Returns undefined if APP_URL is not set (e.g., in e2e tests).
|
|
37
38
|
*/
|
|
38
39
|
const getPasskeyRpID = (): string | undefined => {
|
|
39
|
-
if (!
|
|
40
|
+
if (!appEnv.APP_URL) return undefined;
|
|
40
41
|
try {
|
|
41
|
-
return new URL(
|
|
42
|
+
return new URL(appEnv.APP_URL).hostname;
|
|
42
43
|
} catch {
|
|
43
44
|
return undefined;
|
|
44
45
|
}
|
|
@@ -46,14 +47,15 @@ const getPasskeyRpID = (): string | undefined => {
|
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Get passkey origins array.
|
|
49
|
-
* Returns undefined if
|
|
50
|
+
* Returns undefined if APP_URL is not set (e.g., in e2e tests).
|
|
50
51
|
*/
|
|
51
52
|
const getPasskeyOrigins = (): string[] | undefined => {
|
|
52
|
-
if (!
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
if (!appEnv.APP_URL) return undefined;
|
|
54
|
+
try {
|
|
55
|
+
return [new URL(appEnv.APP_URL).origin];
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
57
59
|
};
|
|
58
60
|
const MAGIC_LINK_EXPIRES_IN = 900;
|
|
59
61
|
// OTP expiration time (in seconds) - 5 minutes for mobile OTP verification
|
|
@@ -81,8 +83,7 @@ export function defineConfig(customOptions: CustomBetterAuthOptions) {
|
|
|
81
83
|
},
|
|
82
84
|
},
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
baseURL: authEnv.NEXT_PUBLIC_AUTH_URL,
|
|
86
|
+
baseURL: appEnv.APP_URL,
|
|
86
87
|
secret: authEnv.AUTH_SECRET,
|
|
87
88
|
trustedOrigins: getTrustedOrigins(enabledSSOProviders),
|
|
88
89
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GenericOAuthConfig } from 'better-auth/plugins';
|
|
2
2
|
import type { SocialProviders } from 'better-auth/social-providers';
|
|
3
3
|
|
|
4
|
+
import { appEnv } from '@/envs/app';
|
|
4
5
|
import { authEnv } from '@/envs/auth';
|
|
5
6
|
import { BUILTIN_BETTER_AUTH_PROVIDERS } from '@/libs/better-auth/constants';
|
|
6
7
|
import { parseSSOProviders } from '@/libs/better-auth/utils/server';
|
|
@@ -106,7 +107,7 @@ export const initBetterAuthSSOProviders = () => {
|
|
|
106
107
|
if (config) {
|
|
107
108
|
// the generic oidc callback url is /api/auth/oauth2/callback/{providerId}
|
|
108
109
|
// different from builtin providers' /api/auth/callback/{providerId}
|
|
109
|
-
config.redirectURI = `${
|
|
110
|
+
config.redirectURI = `${appEnv.APP_URL}/api/auth/callback/${definition.id}`;
|
|
110
111
|
genericOAuthProviders.push(config);
|
|
111
112
|
}
|
|
112
113
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appEnv } from '@/envs/app';
|
|
1
2
|
import { authEnv } from '@/envs/auth';
|
|
2
3
|
import { getRedisConfig } from '@/envs/redis';
|
|
3
4
|
import { initializeRedis, isRedisEnabled } from '@/libs/redis';
|
|
@@ -48,8 +49,7 @@ export const getTrustedOrigins = (enabledSSOProviders: string[]) => {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const defaults = [
|
|
51
|
-
|
|
52
|
-
normalizeOrigin(process.env.APP_URL),
|
|
52
|
+
normalizeOrigin(appEnv.APP_URL),
|
|
53
53
|
normalizeOrigin(process.env.VERCEL_BRANCH_URL),
|
|
54
54
|
normalizeOrigin(process.env.VERCEL_URL),
|
|
55
55
|
MOBILE_APP_SCHEME,
|
|
@@ -237,9 +237,8 @@ export function defineConfig() {
|
|
|
237
237
|
// ref: https://authjs.dev/getting-started/session-management/protecting
|
|
238
238
|
if (isProtected) {
|
|
239
239
|
logNextAuth('Request a protected route, redirecting to sign-in page');
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
const nextLoginUrl = new URL('/next-auth/signin', authUrl);
|
|
240
|
+
const callbackUrl = `${appEnv.APP_URL}${req.nextUrl.pathname}${req.nextUrl.search}`;
|
|
241
|
+
const nextLoginUrl = new URL('/next-auth/signin', appEnv.APP_URL);
|
|
243
242
|
nextLoginUrl.searchParams.set('callbackUrl', callbackUrl);
|
|
244
243
|
const hl = req.nextUrl.searchParams.get('hl');
|
|
245
244
|
if (hl) {
|
|
@@ -325,9 +324,8 @@ export function defineConfig() {
|
|
|
325
324
|
// If request a protected route, redirect to sign-in page
|
|
326
325
|
if (isProtected) {
|
|
327
326
|
logBetterAuth('Request a protected route, redirecting to sign-in page');
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const signInUrl = new URL('/signin', authUrl);
|
|
327
|
+
const callbackUrl = `${appEnv.APP_URL}${req.nextUrl.pathname}${req.nextUrl.search}`;
|
|
328
|
+
const signInUrl = new URL('/signin', appEnv.APP_URL);
|
|
331
329
|
signInUrl.searchParams.set('callbackUrl', callbackUrl);
|
|
332
330
|
const hl = req.nextUrl.searchParams.get('hl');
|
|
333
331
|
if (hl) {
|
|
@@ -285,6 +285,80 @@ describe('Topic Router Integration Tests', () => {
|
|
|
285
285
|
});
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
+
describe('batchDeleteByAgentId', () => {
|
|
289
|
+
it('should batch delete topics by agentId (new data)', async () => {
|
|
290
|
+
const caller = topicRouter.createCaller(createTestContext(userId));
|
|
291
|
+
|
|
292
|
+
// Create topics with agentId directly (new data structure)
|
|
293
|
+
const topicId1 = await caller.createTopic({
|
|
294
|
+
title: 'Agent Topic 1',
|
|
295
|
+
agentId: testAgentId,
|
|
296
|
+
});
|
|
297
|
+
const topicId2 = await caller.createTopic({
|
|
298
|
+
title: 'Agent Topic 2',
|
|
299
|
+
agentId: testAgentId,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Batch delete by agentId
|
|
303
|
+
await caller.batchDeleteByAgentId({
|
|
304
|
+
agentId: testAgentId,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const remainingTopics = await serverDB.select().from(topics).where(eq(topics.userId, userId));
|
|
308
|
+
|
|
309
|
+
expect(remainingTopics).toHaveLength(0);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should batch delete topics by agentId (legacy sessionId data)', async () => {
|
|
313
|
+
const caller = topicRouter.createCaller(createTestContext(userId));
|
|
314
|
+
|
|
315
|
+
// Create topics with sessionId (legacy data structure)
|
|
316
|
+
await caller.createTopic({
|
|
317
|
+
title: 'Legacy Topic 1',
|
|
318
|
+
sessionId: testSessionId,
|
|
319
|
+
});
|
|
320
|
+
await caller.createTopic({
|
|
321
|
+
title: 'Legacy Topic 2',
|
|
322
|
+
sessionId: testSessionId,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Batch delete by agentId should also delete legacy topics via sessionId mapping
|
|
326
|
+
await caller.batchDeleteByAgentId({
|
|
327
|
+
agentId: testAgentId,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const remainingTopics = await serverDB
|
|
331
|
+
.select()
|
|
332
|
+
.from(topics)
|
|
333
|
+
.where(eq(topics.sessionId, testSessionId));
|
|
334
|
+
|
|
335
|
+
expect(remainingTopics).toHaveLength(0);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should batch delete topics by agentId (mixed data)', async () => {
|
|
339
|
+
const caller = topicRouter.createCaller(createTestContext(userId));
|
|
340
|
+
|
|
341
|
+
// Create both new (agentId) and legacy (sessionId) topics
|
|
342
|
+
await caller.createTopic({
|
|
343
|
+
title: 'New Agent Topic',
|
|
344
|
+
agentId: testAgentId,
|
|
345
|
+
});
|
|
346
|
+
await caller.createTopic({
|
|
347
|
+
title: 'Legacy Session Topic',
|
|
348
|
+
sessionId: testSessionId,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Batch delete by agentId should delete both
|
|
352
|
+
await caller.batchDeleteByAgentId({
|
|
353
|
+
agentId: testAgentId,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const remainingTopics = await serverDB.select().from(topics).where(eq(topics.userId, userId));
|
|
357
|
+
|
|
358
|
+
expect(remainingTopics).toHaveLength(0);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
288
362
|
describe('searchTopics', () => {
|
|
289
363
|
it('should search topics using agentId', async () => {
|
|
290
364
|
const caller = topicRouter.createCaller(createTestContext(userId));
|
|
@@ -76,6 +76,12 @@ export const topicRouter = router({
|
|
|
76
76
|
return ctx.topicModel.batchDelete(input.ids);
|
|
77
77
|
}),
|
|
78
78
|
|
|
79
|
+
batchDeleteByAgentId: topicProcedure
|
|
80
|
+
.input(z.object({ agentId: z.string() }))
|
|
81
|
+
.mutation(async ({ input, ctx }) => {
|
|
82
|
+
return ctx.topicModel.batchDeleteByAgentId(input.agentId);
|
|
83
|
+
}),
|
|
84
|
+
|
|
79
85
|
batchDeleteBySessionId: topicProcedure
|
|
80
86
|
.input(
|
|
81
87
|
z.object({
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment node
|
|
3
|
+
*/
|
|
1
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
5
|
|
|
3
6
|
import { AgentRuntimeService } from './AgentRuntimeService';
|
|
@@ -166,7 +169,7 @@ describe('AgentRuntimeService', () => {
|
|
|
166
169
|
it('should initialize with default base URL', () => {
|
|
167
170
|
delete process.env.AGENT_RUNTIME_BASE_URL;
|
|
168
171
|
const newService = new AgentRuntimeService(mockDb, mockUserId);
|
|
169
|
-
expect((newService as any).baseURL).toBe('http://localhost:
|
|
172
|
+
expect((newService as any).baseURL).toBe('http://localhost:3210/api/agent');
|
|
170
173
|
});
|
|
171
174
|
|
|
172
175
|
it('should initialize with custom base URL from environment', () => {
|
|
@@ -10,6 +10,7 @@ import urlJoin from 'url-join';
|
|
|
10
10
|
|
|
11
11
|
import { MessageModel } from '@/database/models/message';
|
|
12
12
|
import { type LobeChatDatabase } from '@/database/type';
|
|
13
|
+
import { appEnv } from '@/envs/app';
|
|
13
14
|
import {
|
|
14
15
|
AgentRuntimeCoordinator,
|
|
15
16
|
type AgentRuntimeCoordinatorOptions,
|
|
@@ -126,7 +127,7 @@ export class AgentRuntimeService {
|
|
|
126
127
|
private stepCallbacks: Map<string, StepLifecycleCallbacks> = new Map();
|
|
127
128
|
private get baseURL() {
|
|
128
129
|
const baseUrl =
|
|
129
|
-
process.env.AGENT_RUNTIME_BASE_URL ||
|
|
130
|
+
process.env.AGENT_RUNTIME_BASE_URL || appEnv.APP_URL || 'http://localhost:3010';
|
|
130
131
|
|
|
131
132
|
return urlJoin(baseUrl, '/api/agent');
|
|
132
133
|
}
|
|
@@ -109,6 +109,10 @@ export class TopicService {
|
|
|
109
109
|
return lambdaClient.topic.batchDeleteBySessionId.mutate({ id: this.toDbSessionId(sessionId) });
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
removeTopicsByAgentId = (agentId: string) => {
|
|
113
|
+
return lambdaClient.topic.batchDeleteByAgentId.mutate({ agentId });
|
|
114
|
+
};
|
|
115
|
+
|
|
112
116
|
batchRemoveTopics = (topics: string[]) => {
|
|
113
117
|
return lambdaClient.topic.batchDelete.mutate({ ids: topics });
|
|
114
118
|
};
|
|
@@ -28,6 +28,7 @@ vi.mock('zustand/traditional');
|
|
|
28
28
|
vi.mock('@/services/topic', () => ({
|
|
29
29
|
topicService: {
|
|
30
30
|
removeTopics: vi.fn(),
|
|
31
|
+
removeTopicsByAgentId: vi.fn(),
|
|
31
32
|
removeAllTopic: vi.fn(),
|
|
32
33
|
removeTopic: vi.fn(),
|
|
33
34
|
cloneTopic: vi.fn(),
|
|
@@ -570,7 +571,7 @@ describe('topic action', () => {
|
|
|
570
571
|
await result.current.removeSessionTopics();
|
|
571
572
|
});
|
|
572
573
|
|
|
573
|
-
expect(topicService.
|
|
574
|
+
expect(topicService.removeTopicsByAgentId).toHaveBeenCalledWith(activeAgentId);
|
|
574
575
|
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
575
576
|
expect(switchTopicSpy).toHaveBeenCalled();
|
|
576
577
|
});
|
|
@@ -567,7 +567,7 @@ export const chatTopic: StateCreator<
|
|
|
567
567
|
const { switchTopic, activeAgentId, refreshTopic } = get();
|
|
568
568
|
if (!activeAgentId) return;
|
|
569
569
|
|
|
570
|
-
await topicService.
|
|
570
|
+
await topicService.removeTopicsByAgentId(activeAgentId);
|
|
571
571
|
await refreshTopic();
|
|
572
572
|
|
|
573
573
|
// switch to default topic
|