diff --git a/api/server/routes/__tests__/config.spec.js b/api/server/routes/__tests__/config.spec.js index e0fc200486..af82399e76 100644 --- a/api/server/routes/__tests__/config.spec.js +++ b/api/server/routes/__tests__/config.spec.js @@ -185,18 +185,22 @@ describe('GET /api/config', () => { expect(response.body).not.toHaveProperty('customFooter'); }); - it('should include public share footer fields when share context is requested', async () => { + it('should not include share-only 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'; + process.env.SANDPACK_BUNDLER_URL = 'https://bundler.test'; + process.env.SANDPACK_STATIC_BUNDLER_URL = 'https://static-bundler.test'; 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('analyticsGtmId'); + expect(response.body).not.toHaveProperty('customFooter'); + expect(response.body).not.toHaveProperty('bundlerURL'); + expect(response.body).not.toHaveProperty('staticBundlerURL'); expect(response.body).not.toHaveProperty('helpAndFaqURL'); expect(response.body).not.toHaveProperty('allowAccountDeletion'); }); diff --git a/api/server/routes/__tests__/share.spec.js b/api/server/routes/__tests__/share.spec.js index ecbd820ae6..c9d6cb0037 100644 --- a/api/server/routes/__tests__/share.spec.js +++ b/api/server/routes/__tests__/share.spec.js @@ -6,6 +6,10 @@ const mockGetSharedLinkExpiration = jest.fn(); const mockGrantCreationPermissions = jest.fn(); const mockUpdateSharedLinkPermissionsExpiration = jest.fn(); const mockSharedLinksAccess = jest.fn((_req, _res, next) => next()); +const mockBuildSharedLinkStartupPayload = jest.fn(); +const mockCanAccessSharedLink = jest.fn((_req, _res, next) => next()); +const mockGetAppConfig = jest.fn(); +const mockGetTenantId = jest.fn(() => undefined); jest.mock('@librechat/api', () => ({ isEnabled: jest.fn(() => true), @@ -16,6 +20,7 @@ jest.mock('@librechat/api', () => ({ ensureLinkPermissions: jest.fn(), isFileSnapshotEnabled: jest.fn(() => true), isFileSnapshotKillSwitchActive: jest.fn(() => false), + buildSharedLinkStartupPayload: (...args) => mockBuildSharedLinkStartupPayload(...args), deleteSharedLinkWithCleanup: jest.fn(), getSharedLinkExpiration: (...args) => mockGetSharedLinkExpiration(...args), isActiveExpirationDate: jest.fn((expiredAt) => expiredAt > new Date()), @@ -23,9 +28,11 @@ jest.mock('@librechat/api', () => ({ jest.mock('@librechat/data-schemas', () => ({ logger: { error: jest.fn(), warn: jest.fn() }, + getTenantId: (...args) => mockGetTenantId(...args), createTempChatExpirationDate: jest.fn(() => new Date('2030-01-01T00:00:00.000Z')), runAsSystem: jest.fn((fn) => fn()), tenantStorage: { run: jest.fn((_ctx, fn) => fn()) }, + SYSTEM_TENANT_ID: '__SYSTEM__', })); jest.mock('librechat-data-provider', () => ({ @@ -84,11 +91,19 @@ jest.mock('~/server/utils/files', () => ({ getContentDisposition: jest.fn((name, disposition = 'attachment') => `${disposition}; ${name}`), })); -jest.mock('~/server/middleware/canAccessSharedLink', () => (_req, _res, next) => next()); +jest.mock( + '~/server/middleware/canAccessSharedLink', + () => + (...args) => + mockCanAccessSharedLink(...args), +); jest.mock('~/server/middleware/optionalShareFileAuth', () => (_req, _res, next) => next()); jest.mock('~/server/middleware/optionalJwtAuth', () => (req, _res, next) => next()); jest.mock('~/server/middleware/requireJwtAuth', () => (req, res, next) => next()); jest.mock('~/server/middleware/config/app', () => (_req, _res, next) => next()); +jest.mock('~/server/services/Config/app', () => ({ + getAppConfig: (...args) => mockGetAppConfig(...args), +})); const { Readable } = require('stream'); const { RetentionMode } = require('librechat-data-provider'); @@ -129,9 +144,22 @@ const buildApp = ({ retentionMode = RetentionMode.TEMPORARY } = {}) => { return app; }; -describe('share routes retention', () => { +describe('share routes', () => { beforeEach(() => { jest.clearAllMocks(); + mockGetTenantId.mockReturnValue(undefined); + mockGetAppConfig.mockResolvedValue({ + interfaceConfig: { + privacyPolicy: { externalUrl: 'https://example.com/privacy' }, + }, + }); + mockBuildSharedLinkStartupPayload.mockReturnValue({ + appTitle: 'Shared Chat', + bundlerURL: 'https://bundler.example.com', + interface: { + privacyPolicy: { externalUrl: 'https://example.com/privacy' }, + }, + }); getRoleByName.mockResolvedValue({ permissions: { SHARED_LINKS: { @@ -142,6 +170,45 @@ describe('share routes retention', () => { mockGrantCreationPermissions.mockResolvedValue(undefined); }); + it('serves shared startup config after shared-link access is granted', async () => { + const response = await request(buildApp()).get('/api/share/share-123/config'); + + expect(response.status).toBe(200); + expect(response.headers['cache-control']).toBe('private, no-store'); + expect(mockCanAccessSharedLink).toHaveBeenCalled(); + expect(mockGetAppConfig).toHaveBeenCalledWith({ baseOnly: true }); + expect(mockBuildSharedLinkStartupPayload).toHaveBeenCalledWith({ + interfaceConfig: { + privacyPolicy: { externalUrl: 'https://example.com/privacy' }, + }, + }); + expect(response.body).toEqual({ + appTitle: 'Shared Chat', + bundlerURL: 'https://bundler.example.com', + interface: { + privacyPolicy: { externalUrl: 'https://example.com/privacy' }, + }, + }); + }); + + it('uses tenant-scoped app config for shared startup config when tenant context is present', async () => { + mockGetTenantId.mockReturnValue('tenant-abc'); + + const response = await request(buildApp()).get('/api/share/share-123/config'); + + expect(response.status).toBe(200); + expect(mockGetAppConfig).toHaveBeenCalledWith({ tenantId: 'tenant-abc' }); + }); + + it('uses base app config for shared startup config in system context', async () => { + mockGetTenantId.mockReturnValue('__SYSTEM__'); + + const response = await request(buildApp()).get('/api/share/share-123/config'); + + expect(response.status).toBe(200); + expect(mockGetAppConfig).toHaveBeenCalledWith({ baseOnly: true }); + }); + it('prevents successful shared message responses from being cached', async () => { getSharedMessages.mockResolvedValue({ shareId: 'share-123', messages: [] }); diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 19f25b844f..f6eb66374e 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -107,9 +107,9 @@ function buildPreLoginPayload() { } /** - * 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. + * Fields shared by authenticated chat and share-view config. Anonymous share + * views receive these through `/api/share/:shareId/config` after share access + * checks, not through the generic startup config endpoint. */ function buildPublicSharePayload() { /** @type {Partial} */ @@ -215,7 +215,6 @@ router.get('/', async function (req, res) { /** @type {Partial} */ const payload = { ...preLoginPayload, - ...(req.query.context === 'share' ? publicSharePayload : {}), socialLogins: baseConfig?.registration?.socialLogins ?? defaultSocialLogins, turnstile: baseConfig?.turnstileConfig, ...(rum ? { rum } : {}), diff --git a/api/server/routes/share.js b/api/server/routes/share.js index 09f84be357..1a15bd2f73 100644 --- a/api/server/routes/share.js +++ b/api/server/routes/share.js @@ -7,6 +7,7 @@ const { ensureLinkPermissions, isFileSnapshotEnabled, isFileSnapshotKillSwitchActive, + buildSharedLinkStartupPayload, deleteSharedLinkWithCleanup, updateSharedLinkPermissionsExpiration, isActiveExpirationDate, @@ -14,8 +15,10 @@ const { } = require('@librechat/api'); const { logger, + getTenantId, runAsSystem, tenantStorage, + SYSTEM_TENANT_ID, createTempChatExpirationDate, } = require('@librechat/data-schemas'); const { FileSources, PermissionTypes, Permissions } = require('librechat-data-provider'); @@ -38,6 +41,7 @@ const optionalShareFileAuth = require('~/server/middleware/optionalShareFileAuth const optionalJwtAuth = require('~/server/middleware/optionalJwtAuth'); const requireJwtAuth = require('~/server/middleware/requireJwtAuth'); const configMiddleware = require('~/server/middleware/config/app'); +const { getAppConfig } = require('~/server/services/Config/app'); const router = express.Router(); const checkSharedLinksAccess = generateCheckAccess({ @@ -76,6 +80,14 @@ const runWithTenant = (tenantId, fn) => * 'failed' on the next poll so the client poller terminates. */ const PREVIEW_LAZY_SWEEP_CUTOFF_MS = 2 * 60 * 1000; +const getShareStartupPayload = async () => { + const tenantId = getTenantId(); + const appConfig = await getAppConfig( + tenantId && tenantId !== SYSTEM_TENANT_ID ? { tenantId } : { baseOnly: true }, + ); + return buildSharedLinkStartupPayload(appConfig); +}; + /** * MIME types that are safe to render inline. Everything else (text/html, SVG, * and other active content) is served as an `attachment` so a public viewer @@ -212,6 +224,17 @@ const streamSharedFile = async (req, res, file, requestedDisposition) => { }; if (allowSharedLinks) { + router.get('/:shareId/config', optionalJwtAuth, canAccessSharedLink, async (_req, res) => { + try { + const payload = await getShareStartupPayload(); + res.set('Cache-Control', 'private, no-store'); + res.status(200).json(payload); + } catch (error) { + logger.error('Error getting shared startup config:', error); + res.status(500).json({ message: 'Error getting shared startup config' }); + } + }); + router.get( '/:shareId', optionalJwtAuth, diff --git a/client/src/components/Artifacts/ArtifactPreview.tsx b/client/src/components/Artifacts/ArtifactPreview.tsx index 8257f76887..c49c22772e 100644 --- a/client/src/components/Artifacts/ArtifactPreview.tsx +++ b/client/src/components/Artifacts/ArtifactPreview.tsx @@ -4,7 +4,7 @@ import type { SandpackProviderProps, SandpackPreviewRef, } from '@codesandbox/sandpack-react/unstyled'; -import type { TStartupConfig } from 'librechat-data-provider'; +import type { SandpackStartupConfig } from '~/utils/artifacts'; import type { ArtifactFiles } from '~/common'; import { sharedFiles, buildSandpackOptions } from '~/utils/artifacts'; @@ -23,7 +23,7 @@ export const ArtifactPreview = memo(function ({ sharedProps: Partial; previewRef: MutableRefObject; currentCode?: string; - startupConfig?: TStartupConfig; + startupConfig?: SandpackStartupConfig; }) { const artifactFiles = useMemo(() => { if (Object.keys(files).length === 0) { diff --git a/client/src/components/Artifacts/ArtifactTabs.tsx b/client/src/components/Artifacts/ArtifactTabs.tsx index 32332215f0..3ebc98a366 100644 --- a/client/src/components/Artifacts/ArtifactTabs.tsx +++ b/client/src/components/Artifacts/ArtifactTabs.tsx @@ -3,11 +3,12 @@ import * as Tabs from '@radix-ui/react-tabs'; import type { SandpackPreviewRef } from '@codesandbox/sandpack-react/unstyled'; import type { editor } from 'monaco-editor'; import type { Artifact } from '~/common'; -import { useCodeState } from '~/Providers/EditorContext'; +import { useGetSharedStartupConfig, useGetStartupConfig } from '~/data-provider'; import useArtifactProps from '~/hooks/Artifacts/useArtifactProps'; import { ArtifactCodeEditor } from './ArtifactCodeEditor'; -import { useGetStartupConfig } from '~/data-provider'; +import { useCodeState } from '~/Providers/EditorContext'; import { ArtifactPreview } from './ArtifactPreview'; +import { useShareContext } from '~/Providers'; export default function ArtifactTabs({ artifact, @@ -19,7 +20,14 @@ export default function ArtifactTabs({ isSharedConvo?: boolean; }) { const { currentCode, setCurrentCode } = useCodeState(); - const { data: startupConfig } = useGetStartupConfig(); + const { shareId } = useShareContext(); + const shouldUseSharedConfig = + isSharedConvo === true && typeof shareId === 'string' && shareId.length > 0; + const { data: startupConfig } = useGetStartupConfig({ enabled: !shouldUseSharedConfig }); + const { data: sharedStartupConfig } = useGetSharedStartupConfig(shareId, { + enabled: shouldUseSharedConfig, + }); + const resolvedStartupConfig = shouldUseSharedConfig ? sharedStartupConfig : startupConfig; const monacoRef = useRef(null); const lastIdRef = useRef(null); @@ -55,7 +63,7 @@ export default function ArtifactTabs({ previewRef={previewRef} sharedProps={sharedProps} currentCode={currentCode} - startupConfig={startupConfig} + startupConfig={resolvedStartupConfig} /> diff --git a/client/src/components/Chat/Footer.tsx b/client/src/components/Chat/Footer.tsx index b841a98819..90d78737b9 100644 --- a/client/src/components/Chat/Footer.tsx +++ b/client/src/components/Chat/Footer.tsx @@ -8,7 +8,11 @@ import { useLocalize } from '~/hooks'; type FooterProps = { className?: string; - startupConfig?: TStartupConfig | null; + startupConfig?: FooterStartupConfig | null; +}; + +type FooterStartupConfig = Pick, 'analyticsGtmId' | 'customFooter'> & { + interface?: Pick, 'privacyPolicy' | 'termsOfService'>; }; function Footer({ className, startupConfig }: FooterProps) { diff --git a/client/src/components/Share/ShareView.tsx b/client/src/components/Share/ShareView.tsx index 68c7a9cd75..20a354742a 100644 --- a/client/src/components/Share/ShareView.tsx +++ b/client/src/components/Share/ShareView.tsx @@ -18,9 +18,9 @@ import { } from '@librechat/client'; import { ThemeSelector, LangSelector } from '~/components/Nav/SettingsTabs/General/Selectors'; import { ShareMessagesProvider } from './ShareMessagesProvider'; +import { useGetSharedStartupConfig } from '~/data-provider'; import { ShareArtifactsContainer } from './ShareArtifacts'; import { useLocalize, useDocumentTitle } from '~/hooks'; -import { useGetStartupConfig } from '~/data-provider'; import { ShareContext } from '~/Providers'; import MessagesView from './MessagesView'; import Footer from '../Chat/Footer'; @@ -29,9 +29,9 @@ import store from '~/store'; function SharedView() { const localize = useLocalize(); - const { data: config } = useGetStartupConfig(undefined, { context: 'share' }); const { theme, setTheme } = useContext(ThemeContext); const { shareId } = useParams(); + const { data: config } = useGetSharedStartupConfig(shareId); const { data, isLoading } = useGetSharedMessages(shareId ?? ''); const dataTree = data && buildTree({ messages: data.messages }); const messagesTree = dataTree?.length === 0 ? null : (dataTree ?? null); diff --git a/client/src/data-provider/Endpoints/queries.ts b/client/src/data-provider/Endpoints/queries.ts index 1ea8b3c0aa..389a9c44cf 100644 --- a/client/src/data-provider/Endpoints/queries.ts +++ b/client/src/data-provider/Endpoints/queries.ts @@ -88,6 +88,9 @@ export const useContextProjectionQuery = ( export const startupConfigKey = (isAuthenticated: boolean, context?: t.StartupConfigContext) => [QueryKeys.startupConfig, isAuthenticated, context ?? 'default'] as const; +export const sharedStartupConfigKey = (shareId?: string) => + [QueryKeys.sharedStartupConfig, shareId ?? ''] as const; + export const useGetStartupConfig = ( config?: UseQueryOptions, options?: { context?: t.StartupConfigContext }, @@ -107,3 +110,26 @@ export const useGetStartupConfig = ( }, ); }; + +export const useGetSharedStartupConfig = ( + shareId?: string, + config?: UseQueryOptions, +): QueryObserverResult => { + const queriesEnabled = useRecoilValue(store.queriesEnabled); + return useQuery( + sharedStartupConfigKey(shareId), + () => dataService.getSharedStartupConfig(shareId ?? ''), + { + staleTime: Infinity, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + refetchOnMount: false, + ...config, + enabled: + (config?.enabled ?? true) === true && + queriesEnabled && + typeof shareId === 'string' && + shareId.length > 0, + }, + ); +}; diff --git a/client/src/utils/artifacts.ts b/client/src/utils/artifacts.ts index 264c8f9e30..b5ff9552e7 100644 --- a/client/src/utils/artifacts.ts +++ b/client/src/utils/artifacts.ts @@ -178,9 +178,14 @@ export const sharedOptions: SandpackProviderProps['options'] = { externalResources: [TAILWIND_CDN], }; +export type SandpackStartupConfig = Pick< + Partial, + 'bundlerURL' | 'staticBundlerURL' +>; + export function buildSandpackOptions( template: SandpackProviderProps['template'], - startupConfig?: TStartupConfig, + startupConfig?: SandpackStartupConfig, ): SandpackProviderProps['options'] { if (!startupConfig) { return sharedOptions; diff --git a/packages/api/src/shared-links/access.test.ts b/packages/api/src/shared-links/access.test.ts index 795ef6c7f1..3f8ea77146 100644 --- a/packages/api/src/shared-links/access.test.ts +++ b/packages/api/src/shared-links/access.test.ts @@ -124,6 +124,20 @@ describe('canAccessSharedLink', () => { expect(res._status).toBe(404); expect(next).not.toHaveBeenCalled(); }); + + test('returns 404 when share is expired but ACL still exists', async () => { + const link = await createTestLink({ expiredAt: new Date('2020-01-01T00:00:00.000Z') }); + await grantPublicViewer(link._id); + process.env.ALLOW_SHARED_LINKS_PUBLIC = 'true'; + + const req = createReq({ params: { shareId: link.shareId } }); + const res = createRes(); + const next = jest.fn(); + await canAccessSharedLink(req, res, next as unknown as NextFunction); + + expect(res._status).toBe(404); + expect(next).not.toHaveBeenCalled(); + }); }); describe('public links', () => { diff --git a/packages/api/src/shared-links/access.ts b/packages/api/src/shared-links/access.ts index d83c5fd24d..3cbb971c3a 100644 --- a/packages/api/src/shared-links/access.ts +++ b/packages/api/src/shared-links/access.ts @@ -1,5 +1,10 @@ import { ResourceType, PermissionBits } from 'librechat-data-provider'; -import { getTenantId, runAsSystem, tenantStorage } from '@librechat/data-schemas'; +import { + getTenantId, + runAsSystem, + tenantStorage, + activeExpirationFilter, +} from '@librechat/data-schemas'; import type { Request, Response, NextFunction } from 'express'; import type { IUser } from '@librechat/data-schemas'; import type { Types, Model } from 'mongoose'; @@ -53,7 +58,10 @@ export function createSharedLinkAccessMiddleware(deps: SharedLinkAccessDeps) { const SharedLink = mg.models.SharedLink as Model; const findShare = async () => - (await SharedLink.findOne({ shareId }).lean()) as RawSharedLink | null; + (await SharedLink.findOne({ + shareId, + ...activeExpirationFilter(), + }).lean()) as RawSharedLink | null; const rawShare = getTenantId() ? await findShare() : await runAsSystem(findShare); if (!rawShare) { diff --git a/packages/api/src/shared-links/config.test.ts b/packages/api/src/shared-links/config.test.ts index 12d4c3d23f..5df601d541 100644 --- a/packages/api/src/shared-links/config.test.ts +++ b/packages/api/src/shared-links/config.test.ts @@ -1,5 +1,5 @@ import type { AppConfig } from '@librechat/data-schemas'; -import { isFileSnapshotEnabled } from './config'; +import { buildSharedLinkStartupPayload, isFileSnapshotEnabled } from './config'; const withSharedLinks = (sharedLinks: unknown): AppConfig => ({ interfaceConfig: { sharedLinks } }) as unknown as AppConfig; @@ -41,3 +41,45 @@ describe('isFileSnapshotEnabled', () => { expect(isFileSnapshotEnabled(withSharedLinks({ snapshotFiles: false }))).toBe(true); }); }); + +describe('buildSharedLinkStartupPayload', () => { + it('builds the share-view startup allowlist', () => { + const payload = buildSharedLinkStartupPayload( + { + interfaceConfig: { + privacyPolicy: { externalUrl: 'https://example.com/privacy' }, + termsOfService: { externalUrl: 'https://example.com/tos' }, + modelSelect: true, + }, + } as AppConfig, + { + ANALYTICS_GTM_ID: 'GTM-XYZ', + APP_TITLE: 'Test Chat', + CUSTOM_FOOTER: 'Shared footer', + SANDPACK_BUNDLER_URL: 'https://bundler.example.com', + SANDPACK_STATIC_BUNDLER_URL: 'https://static-bundler.example.com', + }, + ); + + expect(payload).toEqual({ + appTitle: 'Test Chat', + analyticsGtmId: 'GTM-XYZ', + bundlerURL: 'https://bundler.example.com', + staticBundlerURL: 'https://static-bundler.example.com', + customFooter: 'Shared footer', + interface: { + privacyPolicy: { externalUrl: 'https://example.com/privacy' }, + termsOfService: { externalUrl: 'https://example.com/tos' }, + }, + }); + }); + + it('defaults the app title and omits unrelated interface config', () => { + const payload = buildSharedLinkStartupPayload( + { interfaceConfig: { modelSelect: true } } as AppConfig, + {}, + ); + + expect(payload).toEqual({ appTitle: 'LibreChat' }); + }); +}); diff --git a/packages/api/src/shared-links/config.ts b/packages/api/src/shared-links/config.ts index cd9766b763..1d19c7d22c 100644 --- a/packages/api/src/shared-links/config.ts +++ b/packages/api/src/shared-links/config.ts @@ -1,6 +1,9 @@ +import type { TSharedLinkStartupConfig } from 'librechat-data-provider'; import type { AppConfig } from '@librechat/data-schemas'; import { isEnabled } from '~/utils'; +type SharedLinkStartupEnv = NodeJS.ProcessEnv; + /** * Whether shared links should snapshot the files referenced by the shared chat * snapshot. The `SHARED_LINKS_SNAPSHOT_FILES` env var overrides the yaml @@ -31,3 +34,35 @@ export function isFileSnapshotKillSwitchActive(): boolean { const envValue = process.env.SHARED_LINKS_SNAPSHOT_FILES; return envValue !== undefined && !isEnabled(envValue); } + +export function buildSharedLinkStartupPayload( + appConfig?: AppConfig | null, + env: SharedLinkStartupEnv = process.env, +): TSharedLinkStartupConfig { + const payload: TSharedLinkStartupConfig = { + appTitle: env.APP_TITLE || 'LibreChat', + }; + + if (typeof env.ANALYTICS_GTM_ID === 'string') { + payload.analyticsGtmId = env.ANALYTICS_GTM_ID; + } + if (typeof env.SANDPACK_BUNDLER_URL === 'string') { + payload.bundlerURL = env.SANDPACK_BUNDLER_URL; + } + if (typeof env.SANDPACK_STATIC_BUNDLER_URL === 'string') { + payload.staticBundlerURL = env.SANDPACK_STATIC_BUNDLER_URL; + } + if (typeof env.CUSTOM_FOOTER === 'string') { + payload.customFooter = env.CUSTOM_FOOTER; + } + + const { privacyPolicy, termsOfService } = appConfig?.interfaceConfig ?? {}; + if (privacyPolicy || termsOfService) { + payload.interface = { + ...(privacyPolicy ? { privacyPolicy } : {}), + ...(termsOfService ? { termsOfService } : {}), + }; + } + + return payload; +} diff --git a/packages/data-provider/src/api-endpoints.ts b/packages/data-provider/src/api-endpoints.ts index bf3d3031bd..326e60f644 100644 --- a/packages/data-provider/src/api-endpoints.ts +++ b/packages/data-provider/src/api-endpoints.ts @@ -71,6 +71,7 @@ export const messagesBranch = () => `${messagesRoot}/branch`; const shareRoot = `${BASE_URL}/api/share`; export const shareMessages = (shareId: string) => `${shareRoot}/${shareId}`; +export const sharedStartupConfig = (shareId: string) => `${shareMessages(shareId)}/config`; export const getSharedLink = (conversationId: string) => `${shareRoot}/link/${conversationId}`; export const getSharedLinks = ( pageSize: number, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index eafcc2f97f..2e0439cc0e 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1447,6 +1447,19 @@ export type TStartupConfig = { }; }; +export type TSharedLinkStartupInterface = Pick< + Partial, + 'privacyPolicy' | 'termsOfService' +>; + +export type TSharedLinkStartupConfig = Pick & + Pick< + Partial, + 'analyticsGtmId' | 'bundlerURL' | 'customFooter' | 'staticBundlerURL' + > & { + interface?: TSharedLinkStartupInterface; + }; + export enum OCRStrategy { MISTRAL_OCR = 'mistral_ocr', CUSTOM_OCR = 'custom_ocr', diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 1ffac23dd4..80e5555170 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -64,6 +64,10 @@ export function getSharedMessages(shareId: string): Promise { + return request.get(endpoints.sharedStartupConfig(shareId)); +} + export const listSharedLinks = async ( params: q.SharedLinksListParams, ): Promise => { diff --git a/packages/data-provider/src/keys.ts b/packages/data-provider/src/keys.ts index 54c7b61c25..dde54005a6 100644 --- a/packages/data-provider/src/keys.ts +++ b/packages/data-provider/src/keys.ts @@ -1,6 +1,7 @@ export enum QueryKeys { messages = 'messages', sharedMessages = 'sharedMessages', + sharedStartupConfig = 'sharedStartupConfig', sharedLinks = 'sharedLinks', allConversations = 'allConversations', archivedConversations = 'archivedConversations',