@lobehub/lobehub 2.0.0-next.343 → 2.0.0-next.345

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 (169) hide show
  1. package/.cursor/rules/i18n.mdc +1 -1
  2. package/.cursor/rules/modal-imperative.mdc +162 -0
  3. package/.cursor/rules/rules-index.mdc +1 -0
  4. package/.env.example +0 -14
  5. package/.eslintrc.js +8 -1
  6. package/CHANGELOG.md +66 -0
  7. package/Dockerfile +3 -13
  8. package/README.md +3 -5
  9. package/README.zh-CN.md +3 -5
  10. package/changelog/v1.json +24 -0
  11. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.mdx +11 -42
  12. package/docs/self-hosting/advanced/auth/clerk-to-betterauth.zh-CN.mdx +10 -41
  13. package/e2e/src/support/webServer.ts +2 -0
  14. package/locales/ar/error.json +0 -4
  15. package/locales/bg-BG/error.json +0 -4
  16. package/locales/de-DE/error.json +0 -4
  17. package/locales/en-US/error.json +0 -4
  18. package/locales/es-ES/error.json +0 -4
  19. package/locales/fa-IR/error.json +0 -4
  20. package/locales/fr-FR/error.json +0 -4
  21. package/locales/it-IT/error.json +0 -4
  22. package/locales/ja-JP/error.json +0 -4
  23. package/locales/ko-KR/error.json +0 -4
  24. package/locales/nl-NL/error.json +0 -4
  25. package/locales/pl-PL/error.json +0 -4
  26. package/locales/pt-BR/error.json +0 -4
  27. package/locales/ru-RU/error.json +0 -4
  28. package/locales/tr-TR/error.json +0 -4
  29. package/locales/vi-VN/error.json +0 -4
  30. package/locales/zh-CN/error.json +0 -4
  31. package/locales/zh-TW/error.json +0 -4
  32. package/package.json +7 -9
  33. package/packages/builtin-agents/package.json +2 -0
  34. package/packages/builtin-agents/src/agents/agent-builder/index.ts +4 -2
  35. package/packages/builtin-agents/src/agents/group-agent-builder/index.ts +4 -2
  36. package/packages/builtin-agents/src/agents/page-agent/index.ts +5 -2
  37. package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +161 -12
  38. package/packages/context-engine/src/engine/messages/MessagesEngine.ts +9 -9
  39. package/packages/context-engine/src/providers/GroupContextInjector.ts +19 -33
  40. package/packages/context-engine/src/providers/__tests__/GroupContextInjector.test.ts +79 -43
  41. package/packages/context-engine/src/providers/__tests__/__snapshots__/GroupContextInjector.test.ts.snap +5 -15
  42. package/packages/database/src/repositories/userMemory/__tests__/UserMemoryTopicRepository.test.ts +24 -3
  43. package/packages/model-bank/src/modelProviders/comfyui.ts +0 -1
  44. package/packages/model-bank/src/modelProviders/fal.ts +0 -1
  45. package/packages/types/src/fetch.ts +1 -2
  46. package/packages/utils/src/server/__tests__/auth.test.ts +0 -47
  47. package/packages/utils/src/server/auth.ts +1 -9
  48. package/scripts/_shared/checkDeprecatedClerkEnv.js +42 -0
  49. package/scripts/changelogWorkflow/buildStaticChangelog.ts +2 -1
  50. package/scripts/clerk-to-betterauth/_internal/types.ts +53 -20
  51. package/scripts/clerk-to-betterauth/export-clerk-users-with-api.ts +43 -36
  52. package/scripts/countEnWord.ts +1 -1
  53. package/scripts/electronWorkflow/modifiers/appCode.mts +2 -131
  54. package/scripts/i18nWorkflow/protectedPatterns.ts +1 -2
  55. package/scripts/prebuild.mts +10 -8
  56. package/scripts/serverLauncher/startServer.js +23 -5
  57. package/src/app/(backend)/middleware/auth/index.test.ts +8 -4
  58. package/src/app/(backend)/middleware/auth/index.ts +0 -15
  59. package/src/app/(backend)/middleware/auth/utils.test.ts +0 -28
  60. package/src/app/(backend)/middleware/auth/utils.ts +2 -17
  61. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +3 -51
  62. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -4
  63. package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +7 -6
  64. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -16
  65. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/index.tsx +1 -1
  66. package/src/app/[variants]/(main)/home/features/InputArea/SkillInstallBanner.tsx +13 -13
  67. package/src/app/[variants]/(main)/home/features/RecentPage/Item.tsx +2 -2
  68. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +3 -21
  69. package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -2
  70. package/src/app/[variants]/(main)/settings/security/index.tsx +1 -22
  71. package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +12 -14
  72. package/src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx +8 -14
  73. package/src/app/[variants]/(main)/settings/skill/index.tsx +7 -5
  74. package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +2 -35
  75. package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +0 -20
  76. package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -2
  77. package/src/app/[variants]/(mobile)/me/profile/features/Category.tsx +3 -13
  78. package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +2 -3
  79. package/src/app/[variants]/share/t/[id]/_layout/index.tsx +1 -1
  80. package/src/app/[variants]/share/t/[id]/index.tsx +1 -1
  81. package/src/app/robots.tsx +1 -1
  82. package/src/envs/auth.ts +2 -27
  83. package/src/envs/llm.ts +2 -2
  84. package/src/features/AgentSetting/AgentPlugin/index.tsx +9 -12
  85. package/src/features/ChatInput/ActionBar/Tools/index.tsx +7 -5
  86. package/src/features/ChatMiniMap/utils.ts +1 -1
  87. package/src/features/CommandMenu/SearchResults.tsx +1 -1
  88. package/src/features/Conversation/ChatList/components/AutoScroll/DebugInspector.tsx +166 -0
  89. package/src/features/Conversation/ChatList/components/AutoScroll/index.tsx +86 -0
  90. package/src/features/Conversation/ChatList/components/VirtualizedList.tsx +11 -17
  91. package/src/features/Conversation/Messages/AgentCouncil/components/AutoScrollShadow.tsx +25 -14
  92. package/src/features/Conversation/Messages/AgentCouncil/components/CouncilMember.tsx +1 -1
  93. package/src/features/IntegrationDetailModal/IntegrationDetailContent.tsx +305 -0
  94. package/src/features/IntegrationDetailModal/index.tsx +21 -283
  95. package/src/features/MCPPluginDetail/Deployment/index.tsx +1 -1
  96. package/src/features/MCPPluginDetail/Schema/Prompts.tsx +1 -1
  97. package/src/features/MCPPluginDetail/Schema/Tools.tsx +1 -1
  98. package/src/features/ProfileEditor/AgentTool.tsx +14 -20
  99. package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/NoteFileItem.tsx +1 -1
  100. package/src/features/SkillStore/LobeHubList/index.tsx +50 -87
  101. package/src/features/SkillStore/Search/index.tsx +1 -1
  102. package/src/features/SkillStore/{Content.tsx → SkillStoreContent.tsx} +3 -8
  103. package/src/features/SkillStore/index.tsx +15 -33
  104. package/src/features/User/UserPanel/PanelContent.tsx +0 -8
  105. package/src/features/User/__tests__/PanelContent.test.tsx +1 -35
  106. package/src/features/User/__tests__/UserAvatar.test.tsx +30 -57
  107. package/src/features/User/__tests__/useMenu.test.tsx +2 -43
  108. package/src/layout/AuthProvider/index.tsx +0 -5
  109. package/src/libs/next/config/define-config.ts +6 -0
  110. package/src/libs/next/proxy/createRouteMatcher.test.ts +121 -0
  111. package/src/libs/next/proxy/createRouteMatcher.ts +18 -0
  112. package/src/libs/next/proxy/define-config.ts +4 -53
  113. package/src/libs/next-auth/adapter/index.ts +1 -2
  114. package/src/libs/oidc-provider/provider.test.ts +5 -316
  115. package/src/libs/trpc/lambda/context.test.ts +0 -13
  116. package/src/libs/trpc/lambda/context.ts +3 -22
  117. package/src/libs/trpc/middleware/userAuth.ts +2 -4
  118. package/src/libs/trusted-client/getSessionUser.ts +2 -17
  119. package/src/locales/default/error.ts +0 -6
  120. package/src/locales/default/index.ts +0 -2
  121. package/src/proxy.ts +0 -1
  122. package/src/server/routers/lambda/__tests__/user.test.ts +0 -71
  123. package/src/server/routers/lambda/user.ts +6 -63
  124. package/src/server/services/changelog/index.test.ts +3 -2
  125. package/src/server/services/changelog/index.ts +1 -1
  126. package/src/server/services/user/index.ts +0 -83
  127. package/src/services/chat/index.ts +1 -2
  128. package/src/services/chat/mecha/agentConfigResolver.test.ts +43 -0
  129. package/src/services/chat/mecha/agentConfigResolver.ts +3 -1
  130. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +58 -14
  131. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +10 -2
  132. package/src/store/user/slices/auth/action.test.ts +1 -81
  133. package/src/store/user/slices/auth/action.ts +3 -28
  134. package/src/store/user/slices/auth/initialState.ts +1 -18
  135. package/src/store/user/slices/auth/selectors.test.ts +2 -127
  136. package/src/store/user/slices/auth/selectors.ts +1 -21
  137. package/src/utils/errorResponse.ts +1 -4
  138. package/src/utils/markdownToTxt.ts +20 -0
  139. package/locales/ar/clerk.json +0 -545
  140. package/locales/bg-BG/clerk.json +0 -545
  141. package/locales/de-DE/clerk.json +0 -545
  142. package/locales/en-US/clerk.json +0 -545
  143. package/locales/es-ES/clerk.json +0 -545
  144. package/locales/fa-IR/clerk.json +0 -545
  145. package/locales/fr-FR/clerk.json +0 -545
  146. package/locales/it-IT/clerk.json +0 -545
  147. package/locales/ja-JP/clerk.json +0 -545
  148. package/locales/ko-KR/clerk.json +0 -545
  149. package/locales/nl-NL/clerk.json +0 -545
  150. package/locales/pl-PL/clerk.json +0 -545
  151. package/locales/pt-BR/clerk.json +0 -545
  152. package/locales/ru-RU/clerk.json +0 -545
  153. package/locales/tr-TR/clerk.json +0 -545
  154. package/locales/vi-VN/clerk.json +0 -545
  155. package/locales/zh-CN/clerk.json +0 -545
  156. package/locales/zh-TW/clerk.json +0 -545
  157. package/src/app/(backend)/api/webhooks/clerk/__tests__/fixtures/createUser.json +0 -73
  158. package/src/app/(backend)/api/webhooks/clerk/route.ts +0 -95
  159. package/src/app/(backend)/api/webhooks/clerk/validateRequest.ts +0 -22
  160. package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +0 -27
  161. package/src/app/[variants]/(main)/settings/security/features/ClerkProfile.tsx +0 -67
  162. package/src/features/Conversation/ChatList/components/AutoScroll.tsx +0 -25
  163. package/src/layout/AuthProvider/Clerk/UserUpdater.tsx +0 -40
  164. package/src/layout/AuthProvider/Clerk/index.tsx +0 -54
  165. package/src/layout/AuthProvider/Clerk/useAppearance.ts +0 -133
  166. package/src/libs/clerk-auth/index.test.ts +0 -216
  167. package/src/libs/clerk-auth/index.ts +0 -80
  168. package/src/locales/default/clerk.ts +0 -677
  169. package/src/server/services/user/index.test.ts +0 -220
@@ -1,9 +1,7 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`GroupContextInjector > Edge Cases > should handle empty members array 1`] = `
4
- "Prompt.
5
-
6
- <group_context>
4
+ "<group_context>
7
5
  You are "", acting as a in the multi-agent group "Empty Group".
8
6
  Your internal agent ID is (for system use only, never expose to users).
9
7
 
@@ -24,9 +22,7 @@ Empty group description
24
22
  `;
25
23
 
26
24
  exports[`GroupContextInjector > Identity Rules Section > should always include identity rules 1`] = `
27
- "Base prompt.
28
-
29
- <group_context>
25
+ "<group_context>
30
26
  You are "", acting as a in the multi-agent group "".
31
27
  Your internal agent ID is (for system use only, never expose to users).
32
28
 
@@ -47,9 +43,7 @@ Your internal agent ID is (for system use only, never expose to users).
47
43
  `;
48
44
 
49
45
  exports[`GroupContextInjector > Variable Replacement > should handle config with only group info 1`] = `
50
- "System prompt.
51
-
52
- <group_context>
46
+ "<group_context>
53
47
  You are "", acting as a in the multi-agent group "Test Group".
54
48
  Your internal agent ID is (for system use only, never expose to users).
55
49
 
@@ -70,9 +64,7 @@ Test group description
70
64
  `;
71
65
 
72
66
  exports[`GroupContextInjector > Variable Replacement > should handle config with only identity info 1`] = `
73
- "You are an editor.
74
-
75
- <group_context>
67
+ "<group_context>
76
68
  You are "Editor", acting as a participant in the multi-agent group "".
77
69
  Your internal agent ID is agt_editor (for system use only, never expose to users).
78
70
 
@@ -93,9 +85,7 @@ Your internal agent ID is agt_editor (for system use only, never expose to users
93
85
  `;
94
86
 
95
87
  exports[`GroupContextInjector > Variable Replacement > should handle empty config 1`] = `
96
- "Base prompt.
97
-
98
- <group_context>
88
+ "<group_context>
99
89
  You are "", acting as a in the multi-agent group "".
100
90
  Your internal agent ID is (for system use only, never expose to users).
101
91
 
@@ -50,9 +50,30 @@ describe('UserMemoryTopicRepository', () => {
50
50
 
51
51
  it('should return concatenated user message content', async () => {
52
52
  await serverDB.insert(messages).values([
53
- { id: 'msg-1', content: 'Hello', role: 'user', topicId, userId },
54
- { id: 'msg-2', content: 'Hi there!', role: 'assistant', topicId, userId },
55
- { id: 'msg-3', content: 'How are you?', role: 'user', topicId, userId },
53
+ {
54
+ id: 'msg-1',
55
+ content: 'Hello',
56
+ role: 'user',
57
+ topicId,
58
+ userId,
59
+ createdAt: new Date('2024-01-01'),
60
+ },
61
+ {
62
+ id: 'msg-2',
63
+ content: 'Hi there!',
64
+ role: 'assistant',
65
+ topicId,
66
+ userId,
67
+ createdAt: new Date('2024-01-02'),
68
+ },
69
+ {
70
+ id: 'msg-3',
71
+ content: 'How are you?',
72
+ role: 'user',
73
+ topicId,
74
+ userId,
75
+ createdAt: new Date('2024-01-03'),
76
+ },
56
77
  ]);
57
78
 
58
79
  const result = await repo.getUserMessagesQueryForTopic(topicId);
@@ -12,7 +12,6 @@ const ComfyUI: ModelProviderCard = {
12
12
  chatModels: [],
13
13
  description:
14
14
  'A powerful open-source workflow engine for image, video, and audio generation, supporting models like SD, FLUX, Qwen, Hunyuan, and WAN with node-based editing and private deployment.',
15
- enabled: true,
16
15
  id: 'comfyui',
17
16
  name: 'ComfyUI',
18
17
  settings: {
@@ -6,7 +6,6 @@ import { type ModelProviderCard } from '@/types/llm';
6
6
  const Fal: ModelProviderCard = {
7
7
  chatModels: [],
8
8
  description: 'A generative media platform built for developers.',
9
- enabled: true,
10
9
  id: 'fal',
11
10
  name: 'Fal',
12
11
  settings: {
@@ -5,8 +5,7 @@ export const ChatErrorType = {
5
5
  // ******* Business Error Semantics ******* //
6
6
 
7
7
  InvalidAccessCode: 'InvalidAccessCode', // is in valid password
8
- InvalidClerkUser: 'InvalidClerkUser', // is not Clerk User
9
- FreePlanLimit: 'FreePlanLimit', // is not Clerk User
8
+ FreePlanLimit: 'FreePlanLimit', // Free plan usage limit
10
9
  SubscriptionPlanLimit: 'SubscriptionPlanLimit', // Subscription user limit exceeded
11
10
  SubscriptionKeyMismatch: 'SubscriptionKeyMismatch', // Subscription key mismatch
12
11
 
@@ -4,34 +4,17 @@ import { extractBearerToken, getUserAuth } from '../auth';
4
4
 
5
5
  // Mock auth constants
6
6
  let mockEnableBetterAuth = false;
7
- let mockEnableClerk = false;
8
7
  let mockEnableNextAuth = false;
9
8
 
10
9
  vi.mock('@/envs/auth', () => ({
11
10
  get enableBetterAuth() {
12
11
  return mockEnableBetterAuth;
13
12
  },
14
- get enableClerk() {
15
- return mockEnableClerk;
16
- },
17
13
  get enableNextAuth() {
18
14
  return mockEnableNextAuth;
19
15
  },
20
16
  }));
21
17
 
22
- vi.mock('@/libs/clerk-auth', () => ({
23
- ClerkAuth: class {
24
- async getAuth() {
25
- return {
26
- clerkAuth: {
27
- redirectToSignIn: vi.fn(),
28
- },
29
- userId: 'clerk-user-id',
30
- };
31
- }
32
- },
33
- }));
34
-
35
18
  vi.mock('@/libs/next-auth', () => ({
36
19
  default: {
37
20
  auth: vi.fn().mockResolvedValue({
@@ -62,7 +45,6 @@ describe('getUserAuth', () => {
62
45
  beforeEach(() => {
63
46
  vi.clearAllMocks();
64
47
  mockEnableBetterAuth = false;
65
- mockEnableClerk = false;
66
48
  mockEnableNextAuth = false;
67
49
  });
68
50
 
@@ -70,22 +52,7 @@ describe('getUserAuth', () => {
70
52
  await expect(getUserAuth()).rejects.toThrow('Auth method is not enabled');
71
53
  });
72
54
 
73
- it('should return clerk auth when clerk is enabled', async () => {
74
- mockEnableClerk = true;
75
- mockEnableNextAuth = false;
76
-
77
- const auth = await getUserAuth();
78
-
79
- expect(auth).toEqual({
80
- clerkAuth: {
81
- redirectToSignIn: expect.any(Function),
82
- },
83
- userId: 'clerk-user-id',
84
- });
85
- });
86
-
87
55
  it('should return next auth when next auth is enabled', async () => {
88
- mockEnableClerk = false;
89
56
  mockEnableNextAuth = true;
90
57
 
91
58
  const auth = await getUserAuth();
@@ -100,20 +67,6 @@ describe('getUserAuth', () => {
100
67
  });
101
68
  });
102
69
 
103
- it('should prioritize clerk auth over next auth when both are enabled', async () => {
104
- mockEnableClerk = true;
105
- mockEnableNextAuth = true;
106
-
107
- const auth = await getUserAuth();
108
-
109
- expect(auth).toEqual({
110
- clerkAuth: {
111
- redirectToSignIn: expect.any(Function),
112
- },
113
- userId: 'clerk-user-id',
114
- });
115
- });
116
-
117
70
  it('should return better auth when better auth is enabled', async () => {
118
71
  mockEnableBetterAuth = true;
119
72
 
@@ -1,16 +1,8 @@
1
1
  import { headers } from 'next/headers';
2
2
 
3
- import { enableBetterAuth, enableClerk, enableNextAuth } from '@/envs/auth';
3
+ import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
4
4
 
5
5
  export const getUserAuth = async () => {
6
- if (enableClerk) {
7
- const { ClerkAuth } = await import('@/libs/clerk-auth');
8
-
9
- const clerkAuth = new ClerkAuth();
10
-
11
- return await clerkAuth.getAuth();
12
- }
13
-
14
6
  if (enableBetterAuth) {
15
7
  const { auth: betterAuth } = await import('@/auth');
16
8
 
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared utility to check for deprecated Clerk environment variables.
3
+ * Used by both prebuild.mts (build time) and startServer.js (Docker runtime).
4
+ *
5
+ * IMPORTANT: Keep this file as CommonJS (.js) for compatibility with startServer.js
6
+ */
7
+
8
+ const CLERK_MIGRATION_DOC_URL =
9
+ 'https://lobehub.com/docs/self-hosting/advanced/auth/clerk-to-betterauth';
10
+
11
+ const CLERK_ENV_VARS = [
12
+ 'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY',
13
+ 'CLERK_SECRET_KEY',
14
+ 'CLERK_WEBHOOK_SECRET',
15
+ ];
16
+
17
+ /**
18
+ * Check for deprecated Clerk environment variables and exit if found
19
+ * @param {object} options
20
+ * @param {string} [options.action='redeploy'] - Action hint in error message ('redeploy' or 'restart')
21
+ */
22
+ function checkDeprecatedClerkEnv(options = {}) {
23
+ const { action = 'redeploy' } = options;
24
+ const foundClerkEnvVars = CLERK_ENV_VARS.filter((envVar) => process.env[envVar]);
25
+
26
+ if (foundClerkEnvVars.length > 0) {
27
+ console.error('\n' + '═'.repeat(70));
28
+ console.error('❌ ERROR: Clerk authentication is no longer supported!');
29
+ console.error('═'.repeat(70));
30
+ console.error('\nDetected deprecated Clerk environment variables:');
31
+ for (const envVar of foundClerkEnvVars) {
32
+ console.error(` • ${envVar}`);
33
+ }
34
+ console.error('\nClerk has been removed from LobeChat. Please migrate to Better Auth.');
35
+ console.error(`\n📖 Migration guide: ${CLERK_MIGRATION_DOC_URL}`);
36
+ console.error(`\nAfter migration, remove the Clerk environment variables and ${action}.`);
37
+ console.error('═'.repeat(70) + '\n');
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ module.exports = { checkDeprecatedClerkEnv };
@@ -1,10 +1,11 @@
1
1
  import { consola } from 'consola';
2
2
  import { readJsonSync, writeJSONSync } from 'fs-extra';
3
- import { markdownToTxt } from 'markdown-to-txt';
4
3
  import { existsSync, readFileSync } from 'node:fs';
5
4
  import { resolve } from 'node:path';
6
5
  import semver from 'semver';
7
6
 
7
+ import { markdownToTxt } from '@/utils/markdownToTxt';
8
+
8
9
  import { CHANGELOG_DIR, CHANGELOG_FILE } from './const';
9
10
 
10
11
  export interface ChangelogStaticItem {
@@ -1,5 +1,3 @@
1
- import type { ExternalAccountJSON, UserJSON } from '@clerk/backend';
2
-
3
1
  export type ClerkToBetterAuthMode = 'test' | 'prod';
4
2
  export type DatabaseDriver = 'neon' | 'node';
5
3
 
@@ -19,27 +17,62 @@ export type CSVUserRow = {
19
17
  verified_phone_numbers: string;
20
18
  };
21
19
 
22
- export type ClerkExternalAccount = Pick<
23
- ExternalAccountJSON,
24
- 'id' | 'provider' | 'provider_user_id' | 'approved_scopes'
25
- > & {
20
+ // Clerk API response types (no SDK dependency)
21
+ export interface ClerkApiExternalAccount {
22
+ approved_scopes: string;
23
+ created_at?: number;
24
+ id: string;
25
+ provider: string;
26
+ provider_user_id: string;
27
+ updated_at?: number;
28
+ verification?: { status: string };
29
+ }
30
+
31
+ export interface ClerkApiEmailAddress {
32
+ email_address: string;
33
+ id: string;
34
+ }
35
+
36
+ export interface ClerkApiUser {
37
+ banned: boolean;
38
+ created_at: number;
39
+ email_addresses?: ClerkApiEmailAddress[];
40
+ external_accounts?: ClerkApiExternalAccount[];
41
+ id: string;
42
+ image_url: string;
43
+ lockout_expires_in_seconds: number | null;
44
+ password_enabled: boolean;
45
+ password_last_updated_at: number | null;
46
+ primary_email_address_id: string | null;
47
+ two_factor_enabled: boolean;
48
+ updated_at: number;
49
+ }
50
+
51
+ export interface ClerkApiUserListResponse {
52
+ data: ClerkApiUser[];
53
+ total_count: number;
54
+ }
55
+
56
+ export interface ClerkExternalAccount {
57
+ approved_scopes: string;
26
58
  created_at?: number;
59
+ id: string;
60
+ provider: string;
61
+ provider_user_id: string;
27
62
  updated_at?: number;
28
63
  verificationStatus?: boolean;
29
- };
64
+ }
30
65
 
31
- export type ClerkUser = Pick<
32
- UserJSON,
33
- | 'id'
34
- | 'image_url'
35
- | 'created_at'
36
- | 'updated_at'
37
- | 'password_last_updated_at'
38
- | 'password_enabled'
39
- | 'banned'
40
- | 'two_factor_enabled'
41
- | 'lockout_expires_in_seconds'
42
- > & {
66
+ export interface ClerkUser {
67
+ banned: boolean;
68
+ created_at: number;
43
69
  external_accounts: ClerkExternalAccount[];
70
+ id: string;
71
+ image_url: string;
72
+ lockout_expires_in_seconds: number | null;
73
+ password_enabled: boolean;
74
+ password_last_updated_at: number | null;
44
75
  primaryEmail?: string;
45
- };
76
+ two_factor_enabled: boolean;
77
+ updated_at: number;
78
+ }
@@ -1,10 +1,9 @@
1
1
  /* eslint-disable unicorn/prefer-top-level-await, unicorn/no-process-exit */
2
- import { type User, createClerkClient } from '@clerk/backend';
3
2
  import { writeFile } from 'node:fs/promises';
4
3
 
5
4
  import { getClerkSecret, getMigrationMode, resolveDataPaths } from './_internal/config';
6
5
  import './_internal/env';
7
- import { ClerkUser } from './_internal/types';
6
+ import { ClerkApiUser, ClerkUser } from './_internal/types';
8
7
 
9
8
  /**
10
9
  * Fetch all Clerk users via REST API and persist them into a local JSON file.
@@ -24,50 +23,58 @@ const ORDER_BY = '+created_at';
24
23
  const DEFAULT_OUTPUT_PATH = resolveDataPaths().clerkUsersPath;
25
24
  const formatDuration = (ms: number) => `${(ms / 1000).toFixed(1)}s`;
26
25
 
26
+ const CLERK_API_BASE = 'https://api.clerk.com/v1';
27
+
27
28
  const sleep = (ms: number) =>
28
29
  new Promise<void>((resolve) => {
29
30
  setTimeout(resolve, ms);
30
31
  });
31
32
 
32
- function getClerkClient(secretKey: string) {
33
- return createClerkClient({
34
- secretKey,
33
+ async function fetchClerkApi<T>(secretKey: string, endpoint: string): Promise<T> {
34
+ const response = await fetch(`${CLERK_API_BASE}${endpoint}`, {
35
+ headers: {
36
+ Authorization: `Bearer ${secretKey}`,
37
+ },
35
38
  });
36
- }
37
39
 
38
- function mapClerkUser(user: User): ClerkUser {
39
- const raw = user.raw!;
40
+ if (!response.ok) {
41
+ throw new Error(`Clerk API error: ${response.status} ${response.statusText}`);
42
+ }
43
+
44
+ return response.json() as Promise<T>;
45
+ }
40
46
 
41
- const primaryEmail = raw.email_addresses?.find(
42
- (email) => email.id === raw.primary_email_address_id,
47
+ function mapClerkUser(user: ClerkApiUser): ClerkUser {
48
+ const primaryEmail = user.email_addresses?.find(
49
+ (email) => email.id === user.primary_email_address_id,
43
50
  )?.email_address;
44
51
 
45
52
  return {
46
- banned: raw.banned,
47
- created_at: raw.created_at,
48
- external_accounts: (raw.external_accounts ?? []).map((acc) => ({
53
+ banned: user.banned,
54
+ created_at: user.created_at,
55
+ external_accounts: (user.external_accounts ?? []).map((acc) => ({
49
56
  approved_scopes: acc.approved_scopes,
50
- created_at: (acc as any).created_at,
57
+ created_at: acc.created_at,
51
58
  id: acc.id,
52
59
  provider: acc.provider,
53
60
  provider_user_id: acc.provider_user_id,
54
- updated_at: (acc as any).updated_at,
61
+ updated_at: acc.updated_at,
55
62
  verificationStatus: acc.verification?.status === 'verified',
56
63
  })),
57
- id: raw.id,
58
- image_url: raw.image_url,
59
- lockout_expires_in_seconds: raw.lockout_expires_in_seconds,
60
- password_enabled: raw.password_enabled,
61
- password_last_updated_at: raw.password_last_updated_at,
64
+ id: user.id,
65
+ image_url: user.image_url,
66
+ lockout_expires_in_seconds: user.lockout_expires_in_seconds,
67
+ password_enabled: user.password_enabled,
68
+ password_last_updated_at: user.password_last_updated_at,
62
69
  primaryEmail,
63
- two_factor_enabled: raw.two_factor_enabled,
64
- updated_at: raw.updated_at,
70
+ two_factor_enabled: user.two_factor_enabled,
71
+ updated_at: user.updated_at,
65
72
  } satisfies ClerkUser;
66
73
  }
67
74
 
68
75
  async function fetchClerkUserPage(
69
76
  offset: number,
70
- clerkClient: ReturnType<typeof getClerkClient>,
77
+ secretKey: string,
71
78
  pageIndex: number,
72
79
  ): Promise<ClerkUser[]> {
73
80
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt += 1) {
@@ -76,12 +83,14 @@ async function fetchClerkUserPage(
76
83
  `🚚 [clerk-export] Fetching page #${pageIndex + 1} offset=${offset} limit=${PAGE_SIZE} (attempt ${attempt}/${MAX_RETRIES})`,
77
84
  );
78
85
 
79
- const { data } = await clerkClient.users.getUserList({
80
- limit: PAGE_SIZE,
81
- offset,
82
- orderBy: ORDER_BY,
86
+ const params = new URLSearchParams({
87
+ limit: String(PAGE_SIZE),
88
+ offset: String(offset),
89
+ order_by: ORDER_BY,
83
90
  });
84
91
 
92
+ const data = await fetchClerkApi<ClerkApiUser[]>(secretKey, `/users?${params}`);
93
+
85
94
  console.log(
86
95
  `📥 [clerk-export] Received page #${pageIndex + 1} (${data.length} users) offset=${offset}`,
87
96
  );
@@ -138,16 +147,14 @@ async function runWithConcurrency<T>(
138
147
  }
139
148
 
140
149
  async function fetchAllClerkUsers(secretKey: string): Promise<ClerkUser[]> {
141
- const clerkClient = getClerkClient(secretKey);
142
150
  const userMap = new Map<string, ClerkUser>();
143
151
 
144
- const firstPageResponse = await clerkClient.users.getUserList({
145
- limit: PAGE_SIZE,
146
- offset: 0,
147
- orderBy: ORDER_BY,
148
- });
149
-
150
- const totalCount = firstPageResponse.totalCount ?? firstPageResponse.data.length;
152
+ // Get total count first
153
+ const countResponse = await fetchClerkApi<{ total_count: number }>(
154
+ secretKey,
155
+ '/users/count',
156
+ );
157
+ const totalCount = countResponse.total_count;
151
158
  const totalPages = Math.ceil(totalCount / PAGE_SIZE);
152
159
  const offsets = Array.from({ length: totalPages }, (_, pageIndex) => pageIndex * PAGE_SIZE);
153
160
 
@@ -156,7 +163,7 @@ async function fetchAllClerkUsers(secretKey: string): Promise<ClerkUser[]> {
156
163
  );
157
164
 
158
165
  await runWithConcurrency(offsets, CONCURRENCY, async (offset, index) => {
159
- const page = await fetchClerkUserPage(offset, clerkClient, index);
166
+ const page = await fetchClerkUserPage(offset, secretKey, index);
160
167
 
161
168
  for (const user of page) {
162
169
  userMap.set(user.id, user);
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  // 配置项
5
5
  const config: Config = {
6
6
  dirPath: './locales/en-US', // 替换为你的目录路径
7
- ignoredFiles: ['clerk', 'models', 'providers', 'auth'], // 需要忽略的文件名
7
+ ignoredFiles: ['models', 'providers', 'auth'], // 需要忽略的文件名
8
8
  };
9
9
 
10
10
  interface FileCount {
@@ -138,124 +138,6 @@ const assertSpeedInsightsAndAnalyticsRemoved = (code: string) =>
138
138
  !/import\s+\{\s*SpeedInsights\s*\}\s+from\b/.test(code) &&
139
139
  !/import\s+Analytics\s+from\b/.test(code);
140
140
 
141
- const removeClerkLogic = (code: string) => {
142
- const ast = parse(Lang.Tsx, code);
143
- const root = ast.root();
144
- const edits: Array<{ start: number; end: number; text: string }> = [];
145
-
146
- // Remove Clerk import - try multiple patterns
147
- const clerkImportPatterns = [
148
- { pattern: 'import Clerk from $SOURCE' },
149
- { pattern: "import Clerk from './Clerk'" },
150
- { pattern: "import Clerk from './Clerk/index'" },
151
- ];
152
-
153
- for (const pattern of clerkImportPatterns) {
154
- const clerkImport = root.find({
155
- rule: pattern,
156
- });
157
- if (clerkImport) {
158
- const range = clerkImport.range();
159
- edits.push({ start: range.start.index, end: range.end.index, text: '' });
160
- break;
161
- }
162
- }
163
-
164
- const findClerkIfStatement = () => {
165
- const directMatch = root.find({
166
- rule: {
167
- pattern: 'if (authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH) { $$$ }',
168
- },
169
- });
170
-
171
- if (directMatch) return directMatch;
172
-
173
- const allIfStatements = root.findAll({
174
- rule: {
175
- kind: 'if_statement',
176
- },
177
- });
178
-
179
- for (const ifStmt of allIfStatements) {
180
- const condition = ifStmt.find({
181
- rule: {
182
- pattern: 'authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH',
183
- },
184
- });
185
-
186
- if (condition) return ifStmt;
187
- }
188
-
189
- return null;
190
- };
191
-
192
- const clerkIfStatement = findClerkIfStatement();
193
-
194
- if (clerkIfStatement) {
195
- const ifRange = clerkIfStatement.range();
196
- const elseClause = clerkIfStatement.find({
197
- rule: {
198
- kind: 'else_clause',
199
- },
200
- });
201
-
202
- if (elseClause) {
203
- const elseIfStmt = elseClause.find({
204
- rule: {
205
- kind: 'if_statement',
206
- },
207
- });
208
-
209
- if (elseIfStmt) {
210
- // Promote the first else-if to a top-level if and keep the rest of the chain
211
- const elseRange = elseClause.range();
212
- const replacement = code
213
- .slice(elseRange.start.index, elseRange.end.index)
214
- .replace(/^\s*else\s+/, '');
215
-
216
- edits.push({
217
- start: ifRange.start.index,
218
- end: ifRange.end.index,
219
- text: replacement,
220
- });
221
- } else {
222
- const elseBlock = elseClause.find({
223
- rule: {
224
- kind: 'statement_block',
225
- },
226
- });
227
-
228
- if (elseBlock) {
229
- edits.push({
230
- start: ifRange.start.index,
231
- end: ifRange.end.index,
232
- text: code.slice(elseBlock.range().start.index, elseBlock.range().end.index),
233
- });
234
- } else {
235
- edits.push({ start: ifRange.start.index, end: ifRange.end.index, text: '' });
236
- }
237
- }
238
- } else {
239
- edits.push({ start: ifRange.start.index, end: ifRange.end.index, text: '' });
240
- }
241
- }
242
-
243
- // Apply edits
244
- if (edits.length === 0) return code;
245
-
246
- edits.sort((a, b) => b.start - a.start);
247
- let result = code;
248
- for (const edit of edits) {
249
- result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
250
- }
251
-
252
- return result;
253
- };
254
-
255
- const assertClerkLogicRemoved = (code: string) =>
256
- !/\bNEXT_PUBLIC_ENABLE_CLERK_AUTH\b/.test(code) &&
257
- !/\bauthEnv\.NEXT_PUBLIC_ENABLE_CLERK_AUTH\b/.test(code);
258
-
259
141
  const removeManifestFromMetadata = (code: string) => {
260
142
  const ast = parse(Lang.Tsx, code);
261
143
  const root = ast.root();
@@ -387,17 +269,7 @@ export const modifyAppCode = async (TEMP_DIR: string) => {
387
269
  assertAfter: assertSpeedInsightsAndAnalyticsRemoved,
388
270
  });
389
271
 
390
- // 6. Remove Clerk logic from src/layout/AuthProvider/index.tsx
391
- const authProviderPath = path.join(TEMP_DIR, 'src/layout/AuthProvider/index.tsx');
392
- console.log(' Processing src/layout/AuthProvider/index.tsx...');
393
- await updateFile({
394
- filePath: authProviderPath,
395
- name: 'modifyAppCode:removeClerkLogic',
396
- transformer: removeClerkLogic,
397
- assertAfter: assertClerkLogicRemoved,
398
- });
399
-
400
- // 7. Replace mdx Image component with next/image export
272
+ // 6. Replace mdx Image component with next/image export
401
273
  const mdxImagePath = path.join(TEMP_DIR, 'src/components/mdx/Image.tsx');
402
274
  console.log(' Processing src/components/mdx/Image.tsx...');
403
275
  await writeFileEnsuring({
@@ -407,7 +279,7 @@ export const modifyAppCode = async (TEMP_DIR: string) => {
407
279
  assertAfter: (code) => normalizeEol(code).trim() === "export { default } from 'next/image';",
408
280
  });
409
281
 
410
- // 8. Remove manifest from metadata
282
+ // 7. Remove manifest from metadata
411
283
  const metadataPath = path.join(TEMP_DIR, 'src/app/[variants]/metadata.ts');
412
284
  console.log(' Processing src/app/[variants]/metadata.ts...');
413
285
  await updateFile({
@@ -424,7 +296,6 @@ if (isDirectRun(import.meta.url)) {
424
296
  { lang: Lang.Tsx, path: 'src/layout/GlobalProvider/index.tsx' },
425
297
  { lang: Lang.Tsx, path: 'src/app/[variants]/(main)/settings/features/SettingsContent.tsx' },
426
298
  { lang: Lang.Tsx, path: 'src/app/[variants]/layout.tsx' },
427
- { lang: Lang.Tsx, path: 'src/layout/AuthProvider/index.tsx' },
428
299
  { lang: Lang.Tsx, path: 'src/components/mdx/Image.tsx' },
429
300
  { lang: Lang.Tsx, path: 'src/app/[variants]/metadata.ts' },
430
301
  ]);