@lobehub/lobehub 2.0.0-next.303 → 2.0.0-next.305

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 (87) hide show
  1. package/.github/workflows/manual-build-desktop.yml +11 -1
  2. package/CHANGELOG.md +50 -0
  3. package/apps/desktop/.i18nrc.js +3 -3
  4. package/apps/desktop/electron.vite.config.ts +0 -2
  5. package/apps/desktop/resources/locales/ar/dialog.json +5 -1
  6. package/apps/desktop/resources/locales/ar/menu.json +16 -0
  7. package/apps/desktop/resources/locales/bg-BG/dialog.json +5 -1
  8. package/apps/desktop/resources/locales/bg-BG/menu.json +16 -0
  9. package/apps/desktop/resources/locales/de-DE/dialog.json +5 -1
  10. package/apps/desktop/resources/locales/de-DE/menu.json +16 -0
  11. package/apps/desktop/resources/locales/en/common.json +26 -0
  12. package/apps/desktop/resources/locales/en/dialog.json +27 -0
  13. package/apps/desktop/resources/locales/en/menu.json +73 -0
  14. package/apps/desktop/resources/locales/es-ES/dialog.json +5 -1
  15. package/apps/desktop/resources/locales/es-ES/menu.json +16 -0
  16. package/apps/desktop/resources/locales/fa-IR/dialog.json +5 -1
  17. package/apps/desktop/resources/locales/fa-IR/menu.json +16 -0
  18. package/apps/desktop/resources/locales/fr-FR/dialog.json +5 -1
  19. package/apps/desktop/resources/locales/fr-FR/menu.json +16 -0
  20. package/apps/desktop/resources/locales/it-IT/dialog.json +5 -1
  21. package/apps/desktop/resources/locales/it-IT/menu.json +16 -0
  22. package/apps/desktop/resources/locales/ja-JP/dialog.json +5 -1
  23. package/apps/desktop/resources/locales/ja-JP/menu.json +16 -0
  24. package/apps/desktop/resources/locales/ko-KR/dialog.json +5 -1
  25. package/apps/desktop/resources/locales/ko-KR/menu.json +16 -0
  26. package/apps/desktop/resources/locales/nl-NL/dialog.json +5 -1
  27. package/apps/desktop/resources/locales/nl-NL/menu.json +16 -0
  28. package/apps/desktop/resources/locales/pl-PL/dialog.json +5 -1
  29. package/apps/desktop/resources/locales/pl-PL/menu.json +16 -0
  30. package/apps/desktop/resources/locales/pt-BR/dialog.json +5 -1
  31. package/apps/desktop/resources/locales/pt-BR/menu.json +16 -0
  32. package/apps/desktop/resources/locales/ru-RU/dialog.json +5 -1
  33. package/apps/desktop/resources/locales/ru-RU/menu.json +16 -0
  34. package/apps/desktop/resources/locales/tr-TR/dialog.json +5 -1
  35. package/apps/desktop/resources/locales/tr-TR/menu.json +16 -0
  36. package/apps/desktop/resources/locales/vi-VN/dialog.json +5 -1
  37. package/apps/desktop/resources/locales/vi-VN/menu.json +16 -0
  38. package/apps/desktop/resources/locales/zh-TW/dialog.json +5 -1
  39. package/apps/desktop/resources/locales/zh-TW/menu.json +16 -0
  40. package/apps/desktop/scripts/update-test/README.md +15 -0
  41. package/apps/desktop/src/common/routes.ts +8 -8
  42. package/apps/desktop/src/main/const/dir.ts +2 -2
  43. package/apps/desktop/src/main/const/env.ts +4 -4
  44. package/apps/desktop/src/main/const/store.ts +3 -3
  45. package/apps/desktop/src/main/controllers/AuthCtr.ts +1 -1
  46. package/apps/desktop/src/main/controllers/McpInstallCtr.ts +8 -8
  47. package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +9 -9
  48. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +8 -8
  49. package/apps/desktop/src/main/core/App.ts +9 -9
  50. package/apps/desktop/src/main/core/infrastructure/BackendProxyProtocolManager.ts +7 -6
  51. package/apps/desktop/src/main/core/infrastructure/StaticFileServerManager.ts +2 -2
  52. package/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts +38 -5
  53. package/apps/desktop/src/main/core/ui/ShortcutManager.ts +10 -10
  54. package/apps/desktop/src/main/core/ui/TrayManager.ts +12 -12
  55. package/apps/desktop/src/main/locales/resources.ts +4 -4
  56. package/apps/desktop/src/main/menus/impls/macOS.ts +1 -1
  57. package/apps/desktop/src/main/menus/types.ts +5 -5
  58. package/apps/desktop/src/main/modules/updater/configs.ts +10 -10
  59. package/apps/desktop/src/main/modules/updater/utils.ts +9 -9
  60. package/apps/desktop/src/main/services/fileSrv.ts +62 -62
  61. package/apps/desktop/src/main/shortcuts/config.ts +3 -3
  62. package/apps/desktop/src/main/types/protocol.ts +12 -12
  63. package/apps/desktop/src/main/utils/file-system.ts +2 -2
  64. package/apps/desktop/src/main/utils/logger.ts +4 -4
  65. package/apps/desktop/src/main/utils/protocol.ts +32 -32
  66. package/changelog/v1.json +14 -0
  67. package/locales/en-US/auth.json +5 -0
  68. package/locales/en-US/plugin.json +1 -0
  69. package/locales/zh-CN/auth.json +5 -0
  70. package/locales/zh-CN/discover.json +4 -4
  71. package/locales/zh-CN/plugin.json +1 -0
  72. package/package.json +6 -5
  73. package/packages/builtin-tool-agent-builder/src/ExecutionRuntime/index.ts +362 -30
  74. package/packages/builtin-tool-agent-builder/src/client/Intervention/InstallPlugin.tsx +28 -4
  75. package/packages/builtin-tool-group-management/src/client/Inspector/ExecuteAgentTask/index.tsx +78 -0
  76. package/packages/builtin-tool-group-management/src/client/Inspector/{ExecuteTasks → ExecuteAgentTasks}/index.tsx +1 -5
  77. package/packages/builtin-tool-group-management/src/client/Inspector/index.ts +4 -2
  78. package/packages/database/src/schemas/relations.ts +4 -4
  79. package/scripts/electronWorkflow/buildDesktopChannel.ts +135 -0
  80. package/src/app/[variants]/(main)/_layout/index.tsx +2 -0
  81. package/src/features/Conversation/ChatList/components/AutoScroll.tsx +3 -9
  82. package/src/features/Conversation/ChatList/components/VirtualizedList.tsx +2 -6
  83. package/src/features/DesktopNavigationBridge/index.tsx +0 -9
  84. package/src/features/Electron/AuthRequiredModal/index.tsx +151 -0
  85. package/src/locales/default/auth.ts +6 -0
  86. package/src/locales/default/plugin.ts +1 -0
  87. package/src/utils/errorResponse.ts +21 -1
@@ -0,0 +1,135 @@
1
+ /* eslint-disable unicorn/no-process-exit */
2
+ import fs from 'fs-extra';
3
+ import { execSync } from 'node:child_process';
4
+ import path from 'node:path';
5
+
6
+ type ReleaseChannel = 'stable' | 'beta' | 'nightly';
7
+
8
+ const rootDir = path.resolve(__dirname, '../..');
9
+ const desktopDir = path.join(rootDir, 'apps/desktop');
10
+ const desktopPackageJsonPath = path.join(desktopDir, 'package.json');
11
+ const buildDir = path.join(desktopDir, 'build');
12
+
13
+ const iconTargets = ['icon.png', 'Icon.icns', 'icon.ico'];
14
+
15
+ const isFlag = (value: string) => value.startsWith('-');
16
+
17
+ const parseArgs = (args: string[]) => {
18
+ let channel = '';
19
+ let version = '';
20
+ let keepChanges = false;
21
+
22
+ for (let i = 0; i < args.length; i += 1) {
23
+ const arg = args[i];
24
+
25
+ if (arg === '--channel' || arg === '-c') {
26
+ channel = args[i + 1] ?? '';
27
+ i += 1;
28
+ continue;
29
+ }
30
+
31
+ if (arg === '--version' || arg === '-v') {
32
+ version = args[i + 1] ?? '';
33
+ i += 1;
34
+ continue;
35
+ }
36
+
37
+ if (arg === '--keep-changes') {
38
+ keepChanges = true;
39
+ continue;
40
+ }
41
+
42
+ if (!isFlag(arg)) {
43
+ if (!channel) {
44
+ channel = arg;
45
+ continue;
46
+ }
47
+
48
+ if (!version) {
49
+ version = arg;
50
+ }
51
+ }
52
+ }
53
+
54
+ return { channel, keepChanges, version };
55
+ };
56
+
57
+ const resolveDefaultVersion = () => {
58
+ const rootPackageJsonPath = path.join(rootDir, 'package.json');
59
+ const rootPackageJson = fs.readJsonSync(rootPackageJsonPath);
60
+ return rootPackageJson.version as string | undefined;
61
+ };
62
+
63
+ const backupFile = async (filePath: string) => {
64
+ try {
65
+ return await fs.readFile(filePath);
66
+ } catch {
67
+ return undefined;
68
+ }
69
+ };
70
+
71
+ const restoreFile = async (filePath: string, content?: Buffer) => {
72
+ if (!content) return;
73
+ await fs.writeFile(filePath, content);
74
+ };
75
+
76
+ const validateChannel = (channel: string): channel is ReleaseChannel =>
77
+ channel === 'stable' || channel === 'beta' || channel === 'nightly';
78
+
79
+ const runCommand = (command: string, env?: Record<string, string | undefined>) => {
80
+ execSync(command, {
81
+ cwd: rootDir,
82
+ env: { ...process.env, ...env },
83
+ stdio: 'inherit',
84
+ });
85
+ };
86
+
87
+ const main = async () => {
88
+ const { channel, version: rawVersion, keepChanges } = parseArgs(process.argv.slice(2));
89
+
90
+ if (!validateChannel(channel)) {
91
+ console.error(
92
+ 'Missing or invalid channel. Usage: npm run desktop:build-channel -- <stable|beta|nightly> [version] [--keep-changes]',
93
+ );
94
+ process.exit(1);
95
+ }
96
+
97
+ const version = rawVersion || resolveDefaultVersion();
98
+
99
+ if (!version) {
100
+ console.error('Missing version. Provide it or ensure root package.json has a version.');
101
+ process.exit(1);
102
+ }
103
+
104
+ const packageJsonBackup = await backupFile(desktopPackageJsonPath);
105
+ const iconBackups = await Promise.all(
106
+ iconTargets.map(async (fileName) => ({
107
+ content: await backupFile(path.join(buildDir, fileName)),
108
+ fileName,
109
+ })),
110
+ );
111
+
112
+ console.log(`🚦 CI-style build channel: ${channel}`);
113
+ console.log(`🏷️ Desktop version: ${version}`);
114
+ console.log(`🧩 Keep local changes: ${keepChanges ? 'yes' : 'no'}`);
115
+
116
+ try {
117
+ runCommand(`npm run workflow:set-desktop-version ${version} ${channel}`);
118
+ runCommand('npm run desktop:build', { UPDATE_CHANNEL: channel });
119
+ } catch (error) {
120
+ console.error('❌ Build failed:', error);
121
+ process.exit(1);
122
+ } finally {
123
+ if (!keepChanges) {
124
+ await restoreFile(desktopPackageJsonPath, packageJsonBackup);
125
+ await Promise.all(
126
+ iconBackups.map(({ fileName, content }) =>
127
+ restoreFile(path.join(buildDir, fileName), content),
128
+ ),
129
+ );
130
+ console.log('🧹 Restored local desktop package metadata and icons.');
131
+ }
132
+ }
133
+ };
134
+
135
+ main();
@@ -12,6 +12,7 @@ import Loading from '@/components/Loading/BrandTextLoading';
12
12
  import { isDesktop } from '@/const/version';
13
13
  import { BANNER_HEIGHT } from '@/features/AlertBanner/CloudBanner';
14
14
  import DesktopNavigationBridge from '@/features/DesktopNavigationBridge';
15
+ import AuthRequiredModal from '@/features/Electron/AuthRequiredModal';
15
16
  import TitleBar from '@/features/Electron/titlebar/TitleBar';
16
17
  import HotkeyHelperPanel from '@/features/HotkeyHelperPanel';
17
18
  import NavPanel from '@/features/NavPanel';
@@ -45,6 +46,7 @@ const Layout: FC = () => {
45
46
  {isDesktop && <TitleBar />}
46
47
  {isDesktop && <DesktopAutoOidcOnFirstOpen />}
47
48
  {isDesktop && <DesktopNavigationBridge />}
49
+ {isDesktop && <AuthRequiredModal />}
48
50
  {showCloudPromotion && <CloudBanner />}
49
51
  </Suspense>
50
52
  <DndContextWrapper>
@@ -2,19 +2,13 @@
2
2
 
3
3
  import { memo, useEffect } from 'react';
4
4
 
5
- import { useConversationStore, virtuaListSelectors } from '../../store';
5
+ import { messageStateSelectors, useConversationStore, virtuaListSelectors } from '../../store';
6
6
  import BackBottom from './BackBottom';
7
7
 
8
- interface AutoScrollProps {
9
- /**
10
- * Whether AI is generating (for auto-scroll during generation)
11
- */
12
- isGenerating?: boolean;
13
- }
14
-
15
- const AutoScroll = memo<AutoScrollProps>(({ isGenerating }) => {
8
+ const AutoScroll = memo(() => {
16
9
  const atBottom = useConversationStore(virtuaListSelectors.atBottom);
17
10
  const isScrolling = useConversationStore(virtuaListSelectors.isScrolling);
11
+ const isGenerating = useConversationStore(messageStateSelectors.isAIGenerating);
18
12
  const scrollToBottom = useConversationStore((s) => s.scrollToBottom);
19
13
 
20
14
  useEffect(() => {
@@ -10,10 +10,6 @@ import AutoScroll from './AutoScroll';
10
10
 
11
11
  interface VirtualizedListProps {
12
12
  dataSource: string[];
13
- /**
14
- * Whether AI is generating (for auto-scroll)
15
- */
16
- isGenerating?: boolean;
17
13
  itemContent: (index: number, data: string) => ReactNode;
18
14
  }
19
15
 
@@ -22,7 +18,7 @@ interface VirtualizedListProps {
22
18
  *
23
19
  * Based on ConversationStore data flow, no dependency on global ChatStore.
24
20
  */
25
- const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent, isGenerating }) => {
21
+ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent }) => {
26
22
  const virtuaRef = useRef<VListHandle>(null);
27
23
  const prevDataLengthRef = useRef(dataSource.length);
28
24
  const scrollEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -154,7 +150,7 @@ const VirtualizedList = memo<VirtualizedListProps>(({ dataSource, itemContent, i
154
150
  position: 'relative',
155
151
  }}
156
152
  >
157
- <AutoScroll isGenerating={isGenerating} />
153
+ <AutoScroll />
158
154
  </WideScreenContainer>
159
155
  </>
160
156
  );
@@ -4,8 +4,6 @@ import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
4
4
  import { memo, useCallback } from 'react';
5
5
  import { useNavigate } from 'react-router-dom';
6
6
 
7
- import { clearDesktopOnboardingCompleted } from '@/app/[variants]/(desktop)/desktop-onboarding/storage';
8
-
9
7
  const DesktopNavigationBridge = memo(() => {
10
8
  const navigate = useNavigate();
11
9
 
@@ -19,13 +17,6 @@ const DesktopNavigationBridge = memo(() => {
19
17
 
20
18
  useWatchBroadcast('navigate', handleNavigate);
21
19
 
22
- const handleAuthorizationRequired = useCallback(() => {
23
- clearDesktopOnboardingCompleted();
24
- navigate('/desktop-onboarding#5', { replace: true });
25
- }, [navigate]);
26
-
27
- useWatchBroadcast('authorizationRequired', handleAuthorizationRequired);
28
-
29
20
  return null;
30
21
  });
31
22
 
@@ -0,0 +1,151 @@
1
+ 'use client';
2
+
3
+ import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
4
+ import { Button, Flexbox, Icon, type ModalInstance, createModal } from '@lobehub/ui';
5
+ import { AlertCircle, LogIn } from 'lucide-react';
6
+ import { type ReactNode, memo, useCallback, useRef, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ import { useElectronStore } from '@/store/electron';
10
+
11
+ interface ModalUpdateOptions {
12
+ closable?: boolean;
13
+ keyboard?: boolean;
14
+ maskClosable?: boolean;
15
+ title?: ReactNode;
16
+ }
17
+
18
+ interface AuthRequiredModalContentProps {
19
+ onClose: () => void;
20
+ setModalProps: (props: ModalUpdateOptions) => void;
21
+ }
22
+
23
+ const AuthRequiredModalContent = memo<AuthRequiredModalContentProps>(
24
+ ({ onClose, setModalProps }) => {
25
+ const { t } = useTranslation('auth');
26
+ const [isSigningIn, setIsSigningIn] = useState(false);
27
+ const isClosingRef = useRef(false);
28
+
29
+ const [dataSyncConfig, connectRemoteServer, refreshServerConfig, clearRemoteServerSyncError] =
30
+ useElectronStore((s) => [
31
+ s.dataSyncConfig,
32
+ s.connectRemoteServer,
33
+ s.refreshServerConfig,
34
+ s.clearRemoteServerSyncError,
35
+ ]);
36
+
37
+ // Update modal props based on signing in state
38
+ setModalProps({
39
+ closable: !isSigningIn,
40
+ keyboard: !isSigningIn,
41
+ maskClosable: !isSigningIn,
42
+ title: (
43
+ <Flexbox align="center" gap={8} horizontal>
44
+ <Icon icon={AlertCircle} />
45
+ {t('authModal.title')}
46
+ </Flexbox>
47
+ ),
48
+ });
49
+
50
+ // Listen for successful authorization to close the modal
51
+ useWatchBroadcast('authorizationSuccessful', async () => {
52
+ if (isClosingRef.current) return;
53
+ isClosingRef.current = true;
54
+ setIsSigningIn(false);
55
+ onClose();
56
+ await refreshServerConfig();
57
+ });
58
+
59
+ // Listen for authorization failure
60
+ useWatchBroadcast('authorizationFailed', () => {
61
+ setIsSigningIn(false);
62
+ });
63
+
64
+ const handleSignIn = useCallback(async () => {
65
+ setIsSigningIn(true);
66
+ clearRemoteServerSyncError();
67
+
68
+ await connectRemoteServer({
69
+ remoteServerUrl: dataSyncConfig?.remoteServerUrl,
70
+ storageMode: dataSyncConfig?.storageMode || 'cloud',
71
+ });
72
+ }, [clearRemoteServerSyncError, connectRemoteServer, dataSyncConfig]);
73
+
74
+ const handleLater = useCallback(() => {
75
+ if (isClosingRef.current) return;
76
+ isClosingRef.current = true;
77
+ onClose();
78
+ }, [onClose]);
79
+
80
+ return (
81
+ <Flexbox gap={16} style={{ padding: 16 }}>
82
+ <p style={{ margin: 0 }}>{t('authModal.description')}</p>
83
+ <Flexbox gap={8} horizontal justify="flex-end">
84
+ <Button disabled={isSigningIn} onClick={handleLater}>
85
+ {t('authModal.later')}
86
+ </Button>
87
+ <Button
88
+ icon={<Icon icon={LogIn} />}
89
+ loading={isSigningIn}
90
+ onClick={handleSignIn}
91
+ type="primary"
92
+ >
93
+ {isSigningIn ? t('authModal.signingIn') : t('authModal.signIn')}
94
+ </Button>
95
+ </Flexbox>
96
+ </Flexbox>
97
+ );
98
+ },
99
+ );
100
+
101
+ AuthRequiredModalContent.displayName = 'AuthRequiredModalContent';
102
+
103
+ /**
104
+ * Hook to create and manage the auth required modal
105
+ */
106
+ export const useAuthRequiredModal = () => {
107
+ const instanceRef = useRef<ModalInstance | null>(null);
108
+
109
+ const open = useCallback(() => {
110
+ // Don't open multiple modals
111
+ if (instanceRef.current) return;
112
+
113
+ const setModalProps = (nextProps: ModalUpdateOptions) => {
114
+ instanceRef.current?.update?.(nextProps);
115
+ };
116
+
117
+ const handleClose = () => {
118
+ instanceRef.current?.close();
119
+ instanceRef.current = null;
120
+ };
121
+
122
+ instanceRef.current = createModal({
123
+ children: <AuthRequiredModalContent onClose={handleClose} setModalProps={setModalProps} />,
124
+ closable: false,
125
+ footer: null,
126
+ keyboard: false,
127
+ maskClosable: false,
128
+ title: '',
129
+ });
130
+ }, []);
131
+
132
+ return { open };
133
+ };
134
+
135
+ /**
136
+ * Component that listens for authorizationRequired IPC events and opens the modal
137
+ */
138
+ const AuthRequiredModal = memo(() => {
139
+ const { open } = useAuthRequiredModal();
140
+
141
+ // Listen for IPC event to open the modal
142
+ useWatchBroadcast('authorizationRequired', () => {
143
+ open();
144
+ });
145
+
146
+ return null;
147
+ });
148
+
149
+ AuthRequiredModal.displayName = 'AuthRequiredModal';
150
+
151
+ export default AuthRequiredModal;
@@ -28,6 +28,12 @@ export default {
28
28
  'apikey.list.columns.status': 'Enabled Status',
29
29
  'apikey.list.title': 'API Key List',
30
30
  'apikey.validation.required': 'This field cannot be empty',
31
+ 'authModal.description':
32
+ 'Your login session has expired. Please sign in again to continue using cloud sync features.',
33
+ 'authModal.later': 'Later',
34
+ 'authModal.signIn': 'Sign In Again',
35
+ 'authModal.signingIn': 'Signing in...',
36
+ 'authModal.title': 'Session Expired',
31
37
  'betterAuth.errors.confirmPasswordRequired': 'Please confirm your password',
32
38
  'betterAuth.errors.emailExists': 'This email is already registered. Please sign in instead',
33
39
  'betterAuth.errors.emailInvalid': 'Please enter a valid email address or username',
@@ -70,6 +70,7 @@ export default {
70
70
  'builtins.lobe-group-management.apiName.summarize': 'Summarize conversation',
71
71
  'builtins.lobe-group-management.apiName.vote': 'Start vote',
72
72
  'builtins.lobe-group-management.inspector.broadcast.title': 'Following Agents speak:',
73
+ 'builtins.lobe-group-management.inspector.executeAgentTask.title': 'Assigning task to:',
73
74
  'builtins.lobe-group-management.inspector.executeAgentTasks.title': 'Assigning tasks to:',
74
75
  'builtins.lobe-group-management.inspector.speak.title': 'Designated Agent speaks:',
75
76
  'builtins.lobe-group-management.title': 'Group Coordinator',
@@ -1,6 +1,16 @@
1
1
  import { AgentRuntimeErrorType, type ILobeAgentRuntimeErrorType } from '@lobechat/model-runtime';
2
2
  import { ChatErrorType, type ErrorResponse, type ErrorType } from '@lobechat/types';
3
3
 
4
+ /**
5
+ * Error types that indicate a real authentication failure.
6
+ * When these errors occur, the response will include X-Auth-Required header
7
+ * to signal the client that re-authentication is needed.
8
+ */
9
+ const AUTH_REQUIRED_ERROR_TYPES = new Set<ErrorType>([
10
+ ChatErrorType.Unauthorized,
11
+ ChatErrorType.InvalidClerkUser,
12
+ ]);
13
+
4
14
  const getStatus = (errorType: ILobeAgentRuntimeErrorType | ErrorType) => {
5
15
  // InvalidAccessCode / InvalidAzureAPIKey / InvalidOpenAIAPIKey / InvalidZhipuAPIKey ....
6
16
  if (errorType.toString().includes('Invalid')) return 401;
@@ -71,5 +81,15 @@ export const createErrorResponse = (
71
81
  );
72
82
  }
73
83
 
74
- return new Response(JSON.stringify(data), { status: statusCode });
84
+ const headers: Record<string, string> = {
85
+ 'Content-Type': 'application/json',
86
+ };
87
+
88
+ // Add X-Auth-Required header for real authentication failures
89
+ // This allows the client to distinguish between auth failures and other 401 errors (e.g., invalid API keys)
90
+ if (AUTH_REQUIRED_ERROR_TYPES.has(errorType as ErrorType)) {
91
+ headers['X-Auth-Required'] = 'true';
92
+ }
93
+
94
+ return new Response(JSON.stringify(data), { headers, status: statusCode });
75
95
  };