mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-03 22:32:42 +00:00
🔒 fix: Strip post-login fields from unauthenticated /api/config response (#13102)
* 🔒 fix: Strip post-login fields from unauthenticated /api/config response Follow-up to #12490 reported in #12688. The unauthenticated /api/config response still included fields that are only consumed after login (helpAndFaqURL, sharedLinksEnabled, publicSharedLinksEnabled, showBirthdayIcon, analyticsGtmId, openidReuseTokens, allowAccountDeletion, customFooter, cloudFront). None of these are read by the auth pages (Login, Registration, RequestPasswordReset, ResetPassword, VerifyEmail, TwoFactorScreen, AuthLayout, Footer, SocialLoginRender). Split buildSharedPayload into two helpers: - buildPreLoginPayload returns only the fields the unauthenticated auth pages need (appTitle, server domain, social-login flags, OpenID/SAML labels and image URLs, registration/email/password-reset flags, minPasswordLength, ldap). - buildPostLoginPayload returns the post-login informational fields and is merged into the response only when req.user is present. Also move buildCloudFrontStartupConfig into the authenticated branch: useAppStartup is the only consumer and it runs after login. Tests updated: existing CloudFront and allowAccountDeletion assertions move to the authenticated context, and two new assertions cover the stripped fields (one for the post-login informational fields, one for cloudFront) in the unauthenticated context. Signed-off-by: ChrisJr404 <chris@hacknow.com> * fix: Request share-context startup config * fix: Pass share startup config into footer --------- Signed-off-by: ChrisJr404 <chris@hacknow.com> Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
820bc3bf23
commit
6db059b8a9
8 changed files with 196 additions and 91 deletions
|
|
@ -101,6 +101,9 @@ afterEach(() => {
|
|||
delete process.env.SAML_CERT;
|
||||
delete process.env.SAML_SESSION_SECRET;
|
||||
delete process.env.ALLOW_ACCOUNT_DELETION;
|
||||
delete process.env.ANALYTICS_GTM_ID;
|
||||
delete process.env.CUSTOM_FOOTER;
|
||||
delete process.env.HELP_AND_FAQ_URL;
|
||||
});
|
||||
|
||||
describe('GET /api/config', () => {
|
||||
|
|
@ -156,9 +159,48 @@ describe('GET /api/config', () => {
|
|||
expect(response.body).not.toHaveProperty('bundlerURL');
|
||||
expect(response.body).not.toHaveProperty('staticBundlerURL');
|
||||
expect(response.body).not.toHaveProperty('sharePointFilePickerEnabled');
|
||||
expect(response.body).not.toHaveProperty('sharePointBaseUrl');
|
||||
expect(response.body).not.toHaveProperty('sharePointPickerGraphScope');
|
||||
expect(response.body).not.toHaveProperty('sharePointPickerSharePointScope');
|
||||
expect(response.body).not.toHaveProperty('conversationImportMaxFileSize');
|
||||
});
|
||||
|
||||
it('should strip authenticated-only informational fields from unauthenticated response (#12688)', async () => {
|
||||
process.env.ANALYTICS_GTM_ID = 'GTM-XYZ';
|
||||
process.env.CUSTOM_FOOTER = 'internal footer text';
|
||||
process.env.HELP_AND_FAQ_URL = 'https://internal.example.com/faq';
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
const app = createApp(null);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).not.toHaveProperty('showBirthdayIcon');
|
||||
expect(response.body).not.toHaveProperty('helpAndFaqURL');
|
||||
expect(response.body).not.toHaveProperty('sharedLinksEnabled');
|
||||
expect(response.body).not.toHaveProperty('publicSharedLinksEnabled');
|
||||
expect(response.body).not.toHaveProperty('analyticsGtmId');
|
||||
expect(response.body).not.toHaveProperty('openidReuseTokens');
|
||||
expect(response.body).not.toHaveProperty('allowAccountDeletion');
|
||||
expect(response.body).not.toHaveProperty('customFooter');
|
||||
});
|
||||
|
||||
it('should include public share footer fields when share context is requested', async () => {
|
||||
process.env.ANALYTICS_GTM_ID = 'GTM-XYZ';
|
||||
process.env.CUSTOM_FOOTER = 'public footer text';
|
||||
process.env.HELP_AND_FAQ_URL = 'https://internal.example.com/faq';
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
const app = createApp(null);
|
||||
|
||||
const response = await request(app).get('/api/config?context=share');
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.analyticsGtmId).toBe('GTM-XYZ');
|
||||
expect(response.body.customFooter).toBe('public footer text');
|
||||
expect(response.body).not.toHaveProperty('helpAndFaqURL');
|
||||
expect(response.body).not.toHaveProperty('allowAccountDeletion');
|
||||
});
|
||||
|
||||
it('should include socialLogins and turnstile from base config', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
const app = createApp(null);
|
||||
|
|
@ -206,7 +248,7 @@ describe('GET /api/config', () => {
|
|||
expect(response.body).toHaveProperty('serverDomain');
|
||||
});
|
||||
|
||||
it('should advertise CloudFront cookie refresh only when signed-cookie mode is active', async () => {
|
||||
it('should omit CloudFront cookie refresh from unauthenticated response (#12688)', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
mockGetCloudFrontConfig.mockReturnValue({
|
||||
domain: 'https://cdn.example.com',
|
||||
|
|
@ -219,69 +261,9 @@ describe('GET /api/config', () => {
|
|||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.cloudFront).toEqual({
|
||||
cookieRefresh: {
|
||||
endpoint: '/api/auth/cloudfront/refresh',
|
||||
domain: 'https://cdn.example.com',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should omit CloudFront cookie refresh when signed-cookie mode is inactive', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
mockGetCloudFrontConfig.mockReturnValue({
|
||||
domain: 'https://cdn.example.com',
|
||||
imageSigning: 'url',
|
||||
});
|
||||
const app = createApp(null);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body).not.toHaveProperty('cloudFront');
|
||||
});
|
||||
|
||||
it('should omit CloudFront cookie refresh when cookie mode cannot mint cookies', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
mockGetCloudFrontConfig.mockReturnValue({
|
||||
domain: 'https://cdn.example.com',
|
||||
imageSigning: 'cookies',
|
||||
});
|
||||
const app = createApp(null);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body).not.toHaveProperty('cloudFront');
|
||||
});
|
||||
|
||||
it('should default allowAccountDeletion to true when env var is unset', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
const app = createApp(null);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.allowAccountDeletion).toBe(true);
|
||||
});
|
||||
|
||||
it('should set allowAccountDeletion to false when ALLOW_ACCOUNT_DELETION=false', async () => {
|
||||
process.env.ALLOW_ACCOUNT_DELETION = 'false';
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
const app = createApp(null);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.allowAccountDeletion).toBe(false);
|
||||
});
|
||||
|
||||
it('should set allowAccountDeletion to true when ALLOW_ACCOUNT_DELETION=true', async () => {
|
||||
process.env.ALLOW_ACCOUNT_DELETION = 'true';
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
const app = createApp(null);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.allowAccountDeletion).toBe(true);
|
||||
});
|
||||
|
||||
it('should return 500 when getAppConfig throws', async () => {
|
||||
mockGetAppConfig.mockRejectedValue(new Error('Config service failure'));
|
||||
const app = createApp(null);
|
||||
|
|
@ -395,6 +377,70 @@ describe('GET /api/config', () => {
|
|||
expect(response.body.conversationImportMaxFileSize).toBe(5000000);
|
||||
});
|
||||
|
||||
it('should include post-login informational fields', async () => {
|
||||
process.env.ANALYTICS_GTM_ID = 'GTM-XYZ';
|
||||
process.env.CUSTOM_FOOTER = 'authenticated footer text';
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body).toHaveProperty('helpAndFaqURL');
|
||||
expect(response.body).toHaveProperty('sharedLinksEnabled');
|
||||
expect(response.body).toHaveProperty('publicSharedLinksEnabled');
|
||||
expect(response.body).toHaveProperty('showBirthdayIcon');
|
||||
expect(response.body).toHaveProperty('openidReuseTokens');
|
||||
expect(response.body.analyticsGtmId).toBe('GTM-XYZ');
|
||||
expect(response.body.customFooter).toBe('authenticated footer text');
|
||||
});
|
||||
|
||||
it('should advertise CloudFront cookie refresh when signed-cookie mode is active', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
mockGetCloudFrontConfig.mockReturnValue({
|
||||
domain: 'https://cdn.example.com',
|
||||
imageSigning: 'cookies',
|
||||
cookieDomain: '.example.com',
|
||||
privateKey: 'test-private-key',
|
||||
keyPairId: 'K123ABC',
|
||||
});
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body.cloudFront).toEqual({
|
||||
cookieRefresh: {
|
||||
endpoint: '/api/auth/cloudfront/refresh',
|
||||
domain: 'https://cdn.example.com',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should omit CloudFront cookie refresh when signed-cookie mode is inactive', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
mockGetCloudFrontConfig.mockReturnValue({
|
||||
domain: 'https://cdn.example.com',
|
||||
imageSigning: 'url',
|
||||
});
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body).not.toHaveProperty('cloudFront');
|
||||
});
|
||||
|
||||
it('should omit CloudFront cookie refresh when cookie mode cannot mint cookies', async () => {
|
||||
mockGetAppConfig.mockResolvedValue(baseAppConfig);
|
||||
mockGetCloudFrontConfig.mockReturnValue({
|
||||
domain: 'https://cdn.example.com',
|
||||
imageSigning: 'cookies',
|
||||
});
|
||||
const app = createApp(mockUser);
|
||||
|
||||
const response = await request(app).get('/api/config');
|
||||
|
||||
expect(response.body).not.toHaveProperty('cloudFront');
|
||||
});
|
||||
|
||||
it('should merge per-user balance override into config', async () => {
|
||||
mockGetAppConfig.mockResolvedValue({
|
||||
...baseAppConfig,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,14 @@ function isBirthday() {
|
|||
return today.getMonth() === 1 && today.getDate() === 11;
|
||||
}
|
||||
|
||||
function buildSharedPayload() {
|
||||
/**
|
||||
* Pre-login fields rendered by the unauthenticated login, registration, password-reset,
|
||||
* and email-verification pages. Any field added here is readable by anonymous callers
|
||||
* of `GET /api/config`, so keep this set strictly to what those pages need.
|
||||
*
|
||||
* See client consumers under `client/src/components/Auth/` and `client/src/routes/Layouts/Startup.tsx`.
|
||||
*/
|
||||
function buildPreLoginPayload() {
|
||||
const isOpenIdEnabled =
|
||||
!!process.env.OPENID_CLIENT_ID &&
|
||||
(isEnabled(process.env.OPENID_USE_PKCE) || !!process.env.OPENID_CLIENT_SECRET?.trim()) &&
|
||||
|
|
@ -83,19 +90,6 @@ function buildSharedPayload() {
|
|||
!!process.env.EMAIL_PASSWORD &&
|
||||
!!process.env.EMAIL_FROM,
|
||||
passwordResetEnabled,
|
||||
showBirthdayIcon:
|
||||
isBirthday() ||
|
||||
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
|
||||
process.env.SHOW_BIRTHDAY_ICON === '',
|
||||
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
|
||||
sharedLinksEnabled,
|
||||
publicSharedLinksEnabled,
|
||||
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
|
||||
openidReuseTokens,
|
||||
/** Read inline (not module-level) for per-request evaluation and test isolation */
|
||||
allowAccountDeletion:
|
||||
process.env.ALLOW_ACCOUNT_DELETION === undefined ||
|
||||
isEnabled(process.env.ALLOW_ACCOUNT_DELETION),
|
||||
};
|
||||
|
||||
const minPasswordLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10);
|
||||
|
|
@ -107,6 +101,20 @@ function buildSharedPayload() {
|
|||
payload.ldap = ldap;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public share fields rendered by `client/src/components/Share/ShareView.tsx`.
|
||||
* They remain off the default anonymous config used by login screens, and are
|
||||
* exposed to anonymous callers only when the client asks for share context.
|
||||
*/
|
||||
function buildPublicSharePayload() {
|
||||
/** @type {Partial<TStartupConfig>} */
|
||||
const payload = {
|
||||
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
|
||||
};
|
||||
|
||||
if (typeof process.env.CUSTOM_FOOTER === 'string') {
|
||||
payload.customFooter = process.env.CUSTOM_FOOTER;
|
||||
}
|
||||
|
|
@ -114,6 +122,32 @@ function buildSharedPayload() {
|
|||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-login fields appended only when `req.user` is present. These describe the
|
||||
* authenticated UX (account-settings links, share-link feature flags, birthday icon,
|
||||
* openid token-reuse marker) and are not needed on the pre-login screens, so they
|
||||
* are not exposed to unauthenticated callers.
|
||||
*/
|
||||
function buildPostLoginPayload() {
|
||||
/** @type {Partial<TStartupConfig>} */
|
||||
const payload = {
|
||||
showBirthdayIcon:
|
||||
isBirthday() ||
|
||||
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
|
||||
process.env.SHOW_BIRTHDAY_ICON === '',
|
||||
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
|
||||
sharedLinksEnabled,
|
||||
publicSharedLinksEnabled,
|
||||
openidReuseTokens,
|
||||
/** Read inline (not module-level) for per-request evaluation and test isolation */
|
||||
allowAccountDeletion:
|
||||
process.env.ALLOW_ACCOUNT_DELETION === undefined ||
|
||||
isEnabled(process.env.ALLOW_ACCOUNT_DELETION),
|
||||
};
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function buildBuildInfoPayload(interfaceConfig) {
|
||||
if (interfaceConfig?.buildInfo === false) {
|
||||
return undefined;
|
||||
|
|
@ -168,8 +202,8 @@ function buildCloudFrontStartupConfig() {
|
|||
|
||||
router.get('/', async function (req, res) {
|
||||
try {
|
||||
const sharedPayload = buildSharedPayload();
|
||||
const cloudFront = buildCloudFrontStartupConfig();
|
||||
const preLoginPayload = buildPreLoginPayload();
|
||||
const publicSharePayload = buildPublicSharePayload();
|
||||
const rum = getRumConfig();
|
||||
|
||||
if (!req.user) {
|
||||
|
|
@ -178,10 +212,10 @@ router.get('/', async function (req, res) {
|
|||
|
||||
/** @type {Partial<TStartupConfig>} */
|
||||
const payload = {
|
||||
...sharedPayload,
|
||||
...preLoginPayload,
|
||||
...(req.query.context === 'share' ? publicSharePayload : {}),
|
||||
socialLogins: baseConfig?.registration?.socialLogins ?? defaultSocialLogins,
|
||||
turnstile: baseConfig?.turnstileConfig,
|
||||
...(cloudFront ? { cloudFront } : {}),
|
||||
...(rum ? { rum } : {}),
|
||||
};
|
||||
|
||||
|
|
@ -215,10 +249,13 @@ router.get('/', async function (req, res) {
|
|||
});
|
||||
|
||||
const balanceConfig = getBalanceConfig(appConfig);
|
||||
const cloudFront = buildCloudFrontStartupConfig();
|
||||
|
||||
/** @type {TStartupConfig} */
|
||||
const payload = {
|
||||
...sharedPayload,
|
||||
...preLoginPayload,
|
||||
...publicSharePayload,
|
||||
...buildPostLoginPayload(),
|
||||
socialLogins: appConfig?.registration?.socialLogins ?? defaultSocialLogins,
|
||||
interface: appConfig?.interfaceConfig,
|
||||
turnstile: appConfig?.turnstileConfig,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,19 @@ import React, { useEffect, memo } from 'react';
|
|||
import TagManager from 'react-gtm-module';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
function Footer({ className }: { className?: string }) {
|
||||
const { data: config } = useGetStartupConfig();
|
||||
type FooterProps = {
|
||||
className?: string;
|
||||
startupConfig?: TStartupConfig | null;
|
||||
};
|
||||
|
||||
function Footer({ className, startupConfig }: FooterProps) {
|
||||
const shouldFetchConfig = startupConfig === undefined;
|
||||
const { data: fetchedConfig } = useGetStartupConfig({ enabled: shouldFetchConfig });
|
||||
const config = shouldFetchConfig ? fetchedConfig : startupConfig;
|
||||
const localize = useLocalize();
|
||||
|
||||
const privacyPolicy = config?.interface?.privacyPolicy;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import store from '~/store';
|
|||
|
||||
function SharedView() {
|
||||
const localize = useLocalize();
|
||||
const { data: config } = useGetStartupConfig();
|
||||
const { data: config } = useGetStartupConfig(undefined, { context: 'share' });
|
||||
const { theme, setTheme } = useContext(ThemeContext);
|
||||
const { shareId } = useParams();
|
||||
const { data, isLoading } = useGetSharedMessages(shareId ?? '');
|
||||
|
|
@ -124,7 +124,10 @@ function SharedView() {
|
|||
|
||||
const footer = (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-surface-secondary from-40% to-transparent">
|
||||
<Footer className="pointer-events-auto relative mx-auto flex max-w-[55rem] flex-wrap items-center justify-center gap-2 px-3 pb-4 pt-6 text-center text-xs text-text-secondary" />
|
||||
<Footer
|
||||
startupConfig={config ?? null}
|
||||
className="pointer-events-auto relative mx-auto flex max-w-[55rem] flex-wrap items-center justify-center gap-2 px-3 pb-4 pt-6 text-center text-xs text-text-secondary"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,17 +28,18 @@ export const useGetEndpointsQuery = <TData = t.TEndpointsConfig>(
|
|||
* (chat page) configs are cached independently, preventing stale
|
||||
* unauthenticated config from persisting after login.
|
||||
*/
|
||||
export const startupConfigKey = (isAuthenticated: boolean) =>
|
||||
[QueryKeys.startupConfig, isAuthenticated] as const;
|
||||
export const startupConfigKey = (isAuthenticated: boolean, context?: t.StartupConfigContext) =>
|
||||
[QueryKeys.startupConfig, isAuthenticated, context ?? 'default'] as const;
|
||||
|
||||
export const useGetStartupConfig = (
|
||||
config?: UseQueryOptions<t.TStartupConfig>,
|
||||
options?: { context?: t.StartupConfigContext },
|
||||
): QueryObserverResult<t.TStartupConfig> => {
|
||||
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
|
||||
const user = useRecoilValue<t.TUser | undefined>(store.user);
|
||||
return useQuery<t.TStartupConfig>(
|
||||
startupConfigKey(!!user),
|
||||
() => dataService.getStartupConfig(),
|
||||
startupConfigKey(!!user, options?.context),
|
||||
() => dataService.getStartupConfig({ context: options?.context }),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
refetchOnWindowFocus: false,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { StartupConfigContext } from './config';
|
||||
import type { AssistantsEndpoint } from './schemas';
|
||||
import * as q from './types/queries';
|
||||
import { ResourceType } from './accessPermissions';
|
||||
|
|
@ -212,7 +213,8 @@ export const mcpOAuthBind = (serverName: string) => `${BASE_URL}/api/mcp/${serve
|
|||
export const actionOAuthBind = (actionId: string) =>
|
||||
`${BASE_URL}/api/actions/${actionId}/oauth/bind`;
|
||||
|
||||
export const config = () => `${BASE_URL}/api/config`;
|
||||
export const config = (context?: StartupConfigContext) =>
|
||||
`${BASE_URL}/api/config${buildQuery({ context })}`;
|
||||
|
||||
export const prompts = () => `${BASE_URL}/api/prompts`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1081,6 +1081,8 @@ export type TRumConfig = {
|
|||
environment?: string;
|
||||
};
|
||||
|
||||
export type StartupConfigContext = 'share';
|
||||
|
||||
export type TStartupConfig = {
|
||||
appTitle: string;
|
||||
socialLogins?: string[];
|
||||
|
|
|
|||
|
|
@ -228,12 +228,18 @@ export function cancelMCPOAuth(serverName: string): Promise<m.CancelMCPOAuthResp
|
|||
|
||||
/* Config */
|
||||
|
||||
export const getStartupConfig = (): Promise<
|
||||
export type StartupConfigOptions = {
|
||||
context?: config.StartupConfigContext;
|
||||
};
|
||||
|
||||
export const getStartupConfig = (
|
||||
options?: StartupConfigOptions,
|
||||
): Promise<
|
||||
config.TStartupConfig & {
|
||||
mcpCustomUserVars?: Record<string, { title: string; description: string }>;
|
||||
}
|
||||
> => {
|
||||
return request.get(endpoints.config());
|
||||
return request.get(endpoints.config(options?.context));
|
||||
};
|
||||
|
||||
export const getAIEndpoints = (): Promise<t.TEndpointsConfig> => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue