@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.
Files changed (31) hide show
  1. package/.env.example +0 -3
  2. package/.env.example.development +0 -3
  3. package/CHANGELOG.md +33 -0
  4. package/Dockerfile +1 -2
  5. package/changelog/v1.json +9 -0
  6. package/docs/self-hosting/advanced/auth.mdx +5 -6
  7. package/docs/self-hosting/advanced/auth.zh-CN.mdx +5 -6
  8. package/docs/self-hosting/environment-variables/auth.mdx +0 -7
  9. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +0 -7
  10. package/locales/en-US/discover.json +1 -0
  11. package/locales/zh-CN/discover.json +1 -0
  12. package/package.json +1 -1
  13. package/scripts/prebuild.mts +2 -2
  14. package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +1 -0
  15. package/src/envs/__tests__/app.test.ts +81 -0
  16. package/src/envs/app.ts +14 -2
  17. package/src/envs/auth.test.ts +0 -13
  18. package/src/envs/auth.ts +0 -41
  19. package/src/libs/better-auth/auth-client.ts +0 -9
  20. package/src/libs/better-auth/define-config.ts +13 -12
  21. package/src/libs/better-auth/sso/index.ts +2 -1
  22. package/src/libs/better-auth/utils/config.ts +2 -2
  23. package/src/libs/next/proxy/define-config.ts +4 -6
  24. package/src/locales/default/discover.ts +2 -0
  25. package/src/server/routers/lambda/__tests__/integration/topic.integration.test.ts +74 -0
  26. package/src/server/routers/lambda/topic.ts +6 -0
  27. package/src/server/services/agentRuntime/AgentRuntimeService.test.ts +4 -1
  28. package/src/server/services/agentRuntime/AgentRuntimeService.ts +2 -1
  29. package/src/services/topic/index.ts +4 -0
  30. package/src/store/chat/slices/topic/action.test.ts +2 -1
  31. 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
@@ -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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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
@@ -189,8 +189,7 @@ ENV KEY_VAULTS_SECRET="" \
189
189
 
190
190
  # Better Auth
191
191
  ENV AUTH_SECRET="" \
192
- AUTH_SSO_PROVIDERS="" \
193
- NEXT_PUBLIC_AUTH_URL=""
192
+ AUTH_SSO_PROVIDERS=""
194
193
 
195
194
  # Clerk
196
195
  ENV CLERK_SECRET_KEY="" \
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Sloved the old removeSessionTopics not work."
6
+ ]
7
+ },
8
+ "date": "2026-01-21",
9
+ "version": "2.0.0-next.329"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "features": [
@@ -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
- | `NEXT_PUBLIC_AUTH_URL` | Required | The browser-accessible base URL for Better Auth (e.g., `http://localhost:3010`, `https://lobechat.com`). Optional for Vercel deployments (auto-detected from `VERCEL_URL`) |
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
- | `NEXT_PUBLIC_AUTH_URL` | 必选 | 浏览器可访问的 Better Auth 基础 URL(例如 `http://localhost:3010`、`https://lobechat.com`)。Vercel 部署时可选(会自动从 `VERCEL_URL` 获取) |
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.328",
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",
@@ -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'}`);
@@ -139,6 +139,7 @@ const AssistantItem = memo<DiscoverAssistantItem>(
139
139
  horizontal
140
140
  justify={'space-between'}
141
141
  padding={16}
142
+ style={{ paddingRight: isGroupAgent ? 80 : 16 }}
142
143
  width={'100%'}
143
144
  >
144
145
  <Flexbox
@@ -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
- const vercelUrl = `https://${process.env.VERCEL_URL}`;
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
- ? vercelUrl
32
+ ? getVercelUrl()
21
33
  : process.env.NODE_ENV === 'development'
22
34
  ? 'http://localhost:3010'
23
35
  : 'http://localhost:3210';
@@ -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 AUTH_URL for passkey rpID.
36
- * Returns undefined if AUTH_URL is not set (e.g., in e2e tests).
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 (!authEnv.NEXT_PUBLIC_AUTH_URL) return undefined;
40
+ if (!appEnv.APP_URL) return undefined;
40
41
  try {
41
- return new URL(authEnv.NEXT_PUBLIC_AUTH_URL).hostname;
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 AUTH_URL is not set (e.g., in e2e tests).
50
+ * Returns undefined if APP_URL is not set (e.g., in e2e tests).
50
51
  */
51
52
  const getPasskeyOrigins = (): string[] | undefined => {
52
- if (!authEnv.NEXT_PUBLIC_AUTH_URL) return undefined;
53
- return [
54
- // Web origin
55
- authEnv.NEXT_PUBLIC_AUTH_URL,
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
- // Use renamed env vars (fallback to next-auth vars is handled in src/envs/auth.ts)
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 = `${authEnv.NEXT_PUBLIC_AUTH_URL || ''}/api/auth/callback/${definition.id}`;
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
- authEnv.NEXT_PUBLIC_AUTH_URL,
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 authUrl = authEnv.NEXT_PUBLIC_AUTH_URL;
241
- const callbackUrl = `${authUrl}${req.nextUrl.pathname}${req.nextUrl.search}`;
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 authUrl = authEnv.NEXT_PUBLIC_AUTH_URL;
329
- const callbackUrl = `${authUrl}${req.nextUrl.pathname}${req.nextUrl.search}`;
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) {
@@ -170,6 +170,8 @@ export default {
170
170
 
171
171
  'fork.viewAllForks': 'View all forks',
172
172
 
173
+ 'groupAgents.tag': 'Group',
174
+
173
175
  'home.communityAgents': 'Community Agents',
174
176
 
175
177
  'home.featuredAssistants': 'Featured Agents',
@@ -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:3010/api/agent');
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 || process.env.APP_URL || 'http://localhost:3010';
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.removeTopics).toHaveBeenCalledWith(activeAgentId);
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.removeTopics(activeAgentId);
570
+ await topicService.removeTopicsByAgentId(activeAgentId);
571
571
  await refreshTopic();
572
572
 
573
573
  // switch to default topic