mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-21 15:45:22 +00:00
🪢 feat: show Langfuse session link in shared chats (#15273)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* feat: show Langfuse sessions in shared chats * fix: wait for optional auth before loading shares * fix: require Langfuse access for shared session links * refactor: move shared Langfuse policy into API package * fix: wrap shared chat actions on mobile * fix: coordinate shared Langfuse link loading * perf: batch shared Langfuse capability checks
This commit is contained in:
parent
03fa98eb37
commit
a70bcf66a4
20 changed files with 614 additions and 59 deletions
|
|
@ -18,10 +18,11 @@ import {
|
|||
getLangfuseTenantDestinations,
|
||||
resolveLangfuseTenantDestination,
|
||||
} from '~/langfuse/tenantDestinations';
|
||||
import { getLangfuseDestinationId, scopeHeadersToDestination } from '~/langfuse/destinations';
|
||||
import { redirectPolicyFor, resolveLangfuseHeaders } from '~/langfuse/utils';
|
||||
import { decryptConfigSecret, encryptConfigSecretFields } from './secrets';
|
||||
import { scopeHeadersToDestination } from '~/langfuse/destinations';
|
||||
import { isLangfuseConnectionAvailable } from '~/langfuse/policy';
|
||||
import { resolveLangfuseSessionUrl } from '~/langfuse/session';
|
||||
import { mergeHeaders } from '~/utils/headers';
|
||||
|
||||
const DEFAULT_PRIORITY = 10;
|
||||
|
|
@ -332,34 +333,13 @@ export function createAdminLangfuseHandlers(deps: AdminLangfuseDeps): {
|
|||
}
|
||||
|
||||
try {
|
||||
const stored = readStoredLangfuse(await findBaseConfig());
|
||||
const destination = resolveLangfuseTenantDestination(stored?.destination);
|
||||
const projectId = stored?.projectId?.trim();
|
||||
if (stored?.enabled !== true || !destination || !projectId) {
|
||||
const response: TLangfuseSessionLinkResponse = { url: null };
|
||||
return res.status(200).json(response);
|
||||
}
|
||||
|
||||
const destinationId = getLangfuseDestinationId(destination.baseUrl, projectId);
|
||||
const messages = await getMessages(
|
||||
{
|
||||
user: userId,
|
||||
conversationId,
|
||||
langfuseSampled: true,
|
||||
langfuseDestinationIds: destinationId,
|
||||
},
|
||||
'_id',
|
||||
{ sort: false, limit: 1 },
|
||||
);
|
||||
if (messages.length === 0) {
|
||||
const response: TLangfuseSessionLinkResponse = { url: null };
|
||||
return res.status(200).json(response);
|
||||
}
|
||||
|
||||
const sessionUrl = new URL(destination.baseUrl);
|
||||
const basePath = sessionUrl.pathname.replace(/\/+$/, '');
|
||||
sessionUrl.pathname = `${basePath}/project/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(conversationId)}`;
|
||||
const response: TLangfuseSessionLinkResponse = { url: sessionUrl.toString() };
|
||||
const url = await resolveLangfuseSessionUrl({
|
||||
config: readStoredLangfuse(await findBaseConfig()),
|
||||
conversationId,
|
||||
userId,
|
||||
getMessages,
|
||||
});
|
||||
const response: TLangfuseSessionLinkResponse = { url };
|
||||
return res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error('[adminLangfuse] getSessionLink error:', error);
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export * from './shared-links/service';
|
|||
export * from './shared-links/config';
|
||||
export * from './shared-links/http';
|
||||
export * from './shared-links/protection';
|
||||
export * from './shared-links/session';
|
||||
/* Stream */
|
||||
export * from './stream';
|
||||
/* Diagnostics */
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ export * from './destinations';
|
|||
export * from './feedback';
|
||||
export * from './policy';
|
||||
export * from './trace';
|
||||
export * from './session';
|
||||
|
|
|
|||
49
packages/api/src/langfuse/session.ts
Normal file
49
packages/api/src/langfuse/session.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { MessageMethods } from '@librechat/data-schemas';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
import { resolveLangfuseTenantDestination } from './tenantDestinations';
|
||||
import { getLangfuseDestinationId } from './destinations';
|
||||
import { isLangfuseConnectionAvailable } from './policy';
|
||||
|
||||
export interface LangfuseSessionLinkParams {
|
||||
config: TCustomConfig['langfuse'];
|
||||
conversationId: string;
|
||||
userId: string;
|
||||
getMessages: MessageMethods['getMessages'];
|
||||
}
|
||||
|
||||
export async function resolveLangfuseSessionUrl({
|
||||
config,
|
||||
conversationId,
|
||||
userId,
|
||||
getMessages,
|
||||
}: LangfuseSessionLinkParams): Promise<string | null> {
|
||||
if (!isLangfuseConnectionAvailable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const destination = resolveLangfuseTenantDestination(config?.destination);
|
||||
const projectId = config?.projectId?.trim();
|
||||
if (config?.enabled !== true || !destination || !projectId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const destinationId = getLangfuseDestinationId(destination.baseUrl, projectId);
|
||||
const messages = await getMessages(
|
||||
{
|
||||
user: userId,
|
||||
conversationId,
|
||||
langfuseSampled: true,
|
||||
langfuseDestinationIds: destinationId,
|
||||
},
|
||||
'_id',
|
||||
{ sort: false, limit: 1 },
|
||||
);
|
||||
if (messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sessionUrl = new URL(destination.baseUrl);
|
||||
const basePath = sessionUrl.pathname.replace(/\/+$/, '');
|
||||
sessionUrl.pathname = `${basePath}/project/${encodeURIComponent(projectId)}/sessions/${encodeURIComponent(conversationId)}`;
|
||||
return sessionUrl.toString();
|
||||
}
|
||||
|
|
@ -31,15 +31,47 @@ const userPrincipals = [
|
|||
describe('generateCapabilityCheck', () => {
|
||||
const mockGetUserPrincipals = jest.fn();
|
||||
const mockHasCapabilityForPrincipals = jest.fn();
|
||||
const mockGetHeldCapabilities = jest.fn();
|
||||
|
||||
const { hasCapability, requireCapability, hasConfigCapability } = generateCapabilityCheck({
|
||||
getUserPrincipals: mockGetUserPrincipals,
|
||||
hasCapabilityForPrincipals: mockHasCapabilityForPrincipals,
|
||||
});
|
||||
const { hasCapability, requireCapability, hasConfigCapability, getHeldCapabilities } =
|
||||
generateCapabilityCheck({
|
||||
getUserPrincipals: mockGetUserPrincipals,
|
||||
hasCapabilityForPrincipals: mockHasCapabilityForPrincipals,
|
||||
getHeldCapabilities: mockGetHeldCapabilities,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetUserPrincipals.mockReset();
|
||||
mockHasCapabilityForPrincipals.mockReset();
|
||||
mockGetHeldCapabilities.mockReset();
|
||||
});
|
||||
|
||||
describe('getHeldCapabilities', () => {
|
||||
it('resolves principals once and checks all requested capabilities in one batch', async () => {
|
||||
const capabilities = [
|
||||
SystemCapabilities.ACCESS_ADMIN,
|
||||
SystemCapabilities.MANAGE_CONFIGS,
|
||||
configCapability('langfuse'),
|
||||
];
|
||||
const held = new Set([SystemCapabilities.ACCESS_ADMIN, configCapability('langfuse')]);
|
||||
mockGetUserPrincipals.mockResolvedValue(adminPrincipals);
|
||||
mockGetHeldCapabilities.mockResolvedValue(held);
|
||||
|
||||
const result = await getHeldCapabilities(
|
||||
{ id: 'user-123', role: 'ADMIN', tenantId: 'tenant-1' },
|
||||
capabilities,
|
||||
);
|
||||
|
||||
expect(result).toBe(held);
|
||||
expect(mockGetUserPrincipals).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetHeldCapabilities).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetHeldCapabilities).toHaveBeenCalledWith({
|
||||
principals: adminPrincipals,
|
||||
capabilities,
|
||||
tenantId: 'tenant-1',
|
||||
});
|
||||
expect(mockHasCapabilityForPrincipals).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCapability', () => {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,11 @@ export type HasConfigCapabilityFn = (
|
|||
verb?: 'manage' | 'read',
|
||||
) => Promise<boolean>;
|
||||
|
||||
export type GetHeldCapabilitiesFn = (
|
||||
user: CapabilityUser,
|
||||
capabilities: SystemCapability[],
|
||||
) => Promise<Set<SystemCapability>>;
|
||||
|
||||
/**
|
||||
* Per-request store for caching resolved principals and capability check results.
|
||||
* When running inside an Express request (via `capabilityContextMiddleware`),
|
||||
|
|
@ -141,6 +146,7 @@ export function generateCapabilityCheck(deps: CapabilityDeps): {
|
|||
hasCapability: HasCapabilityFn;
|
||||
requireCapability: RequireCapabilityFn;
|
||||
hasConfigCapability: HasConfigCapabilityFn;
|
||||
getHeldCapabilities: GetHeldCapabilitiesFn;
|
||||
hasAnyConfigReadAccess: (user: CapabilityUser) => Promise<boolean>;
|
||||
getReadableConfigSections: GetReadableConfigSectionsFn;
|
||||
} {
|
||||
|
|
@ -175,6 +181,14 @@ export function generateCapabilityCheck(deps: CapabilityDeps): {
|
|||
return checkAny({ principals, tenantId: user.tenantId });
|
||||
}
|
||||
|
||||
async function getHeldCapabilities(
|
||||
user: CapabilityUser,
|
||||
capabilities: SystemCapability[],
|
||||
): Promise<Set<SystemCapability>> {
|
||||
const principals = await resolvePrincipals(user);
|
||||
return getHeldCaps({ principals, capabilities, tenantId: user.tenantId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves which of `sections` the user can read in a single batched
|
||||
* query, instead of one `hasConfigCapability` round trip per section.
|
||||
|
|
@ -183,17 +197,12 @@ export function generateCapabilityCheck(deps: CapabilityDeps): {
|
|||
user: CapabilityUser,
|
||||
sections: ConfigSection[],
|
||||
): Promise<{ broad: boolean; sections: Set<string> }> {
|
||||
const principals = await resolvePrincipals(user);
|
||||
const capsToCheck = [
|
||||
SystemCapabilities.READ_CONFIGS,
|
||||
SystemCapabilities.MANAGE_CONFIGS,
|
||||
...sections.map(readConfigCapability),
|
||||
];
|
||||
const held = await getHeldCaps({
|
||||
principals,
|
||||
capabilities: capsToCheck,
|
||||
tenantId: user.tenantId,
|
||||
});
|
||||
const held = await getHeldCapabilities(user, capsToCheck);
|
||||
const broad =
|
||||
held.has(SystemCapabilities.READ_CONFIGS) || held.has(SystemCapabilities.MANAGE_CONFIGS);
|
||||
const readableSections = new Set(
|
||||
|
|
@ -298,6 +307,7 @@ export function generateCapabilityCheck(deps: CapabilityDeps): {
|
|||
hasCapability,
|
||||
requireCapability,
|
||||
hasConfigCapability,
|
||||
getHeldCapabilities,
|
||||
hasAnyConfigReadAccess,
|
||||
getReadableConfigSections,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -200,7 +200,11 @@ describe('canAccessSharedLink', () => {
|
|||
await canAccessSharedLink(req, res, next as unknown as NextFunction);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect((req as unknown as Record<string, unknown>).shareResourceId).toBe(link._id.toString());
|
||||
expect(req).toMatchObject({
|
||||
shareResourceId: link._id.toString(),
|
||||
shareConversationId: 'convo1',
|
||||
shareOwnerId: userId.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
test('returns 401 for anonymous access when ALLOW_SHARED_LINKS_PUBLIC is not set', async () => {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ interface RawSharedLink {
|
|||
interface SharedLinkAccessRequest extends Request {
|
||||
shareResourceId?: string;
|
||||
shareTenantId?: string;
|
||||
shareConversationId?: string;
|
||||
shareOwnerId?: string;
|
||||
}
|
||||
|
||||
export interface SharedLinkAccessDeps {
|
||||
|
|
@ -94,6 +96,8 @@ export function createSharedLinkAccessMiddleware(deps: SharedLinkAccessDeps) {
|
|||
const sharedRequest = req as SharedLinkAccessRequest;
|
||||
sharedRequest.shareResourceId = resourceId;
|
||||
sharedRequest.shareTenantId = rawShare.tenantId;
|
||||
sharedRequest.shareConversationId = rawShare.conversationId;
|
||||
sharedRequest.shareOwnerId = rawShare.user;
|
||||
next();
|
||||
};
|
||||
|
||||
|
|
|
|||
107
packages/api/src/shared-links/session.spec.ts
Normal file
107
packages/api/src/shared-links/session.spec.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { configCapability, SystemCapabilities, type MessageMethods } from '@librechat/data-schemas';
|
||||
import { createSharedLangfuseSessionResolver } from './session';
|
||||
|
||||
const getHeldCapabilities = jest.fn();
|
||||
const getMessages = jest.fn() as unknown as MessageMethods['getMessages'];
|
||||
const resolveSessionUrl = jest.fn();
|
||||
|
||||
const config = {
|
||||
enabled: true,
|
||||
destination: 'eu',
|
||||
projectId: 'project-1',
|
||||
};
|
||||
|
||||
function createResolver() {
|
||||
return createSharedLangfuseSessionResolver({
|
||||
getHeldCapabilities,
|
||||
getMessages,
|
||||
resolveSessionUrl,
|
||||
});
|
||||
}
|
||||
|
||||
const validParams = {
|
||||
viewer: { id: 'admin-1', role: 'ADMIN', tenantId: 'tenant-1' },
|
||||
shareTenantId: 'tenant-1',
|
||||
shareConversationId: 'conversation-1',
|
||||
shareOwnerId: 'owner-1',
|
||||
config,
|
||||
};
|
||||
|
||||
describe('createSharedLangfuseSessionResolver', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
getHeldCapabilities.mockResolvedValue(
|
||||
new Set([SystemCapabilities.ACCESS_ADMIN, SystemCapabilities.MANAGE_CONFIGS]),
|
||||
);
|
||||
resolveSessionUrl.mockResolvedValue('https://cloud.langfuse.com/session-1');
|
||||
});
|
||||
|
||||
it('resolves the source session for an authorized same-tenant admin', async () => {
|
||||
const result = await createResolver()(validParams);
|
||||
|
||||
expect(result).toBe('https://cloud.langfuse.com/session-1');
|
||||
expect(getHeldCapabilities).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'admin-1', tenantId: 'tenant-1' }),
|
||||
[
|
||||
SystemCapabilities.ACCESS_ADMIN,
|
||||
SystemCapabilities.MANAGE_CONFIGS,
|
||||
configCapability('langfuse'),
|
||||
],
|
||||
);
|
||||
expect(resolveSessionUrl).toHaveBeenCalledWith({
|
||||
config,
|
||||
conversationId: 'conversation-1',
|
||||
userId: 'owner-1',
|
||||
getMessages,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a viewer from another tenant before capability checks', async () => {
|
||||
const result = await createResolver()({
|
||||
...validParams,
|
||||
viewer: { ...validParams.viewer, tenantId: 'tenant-2' },
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(getHeldCapabilities).not.toHaveBeenCalled();
|
||||
expect(resolveSessionUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a viewer without admin access', async () => {
|
||||
getHeldCapabilities.mockResolvedValue(new Set([SystemCapabilities.MANAGE_CONFIGS]));
|
||||
|
||||
const result = await createResolver()(validParams);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(resolveSessionUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects an admin without Langfuse config access', async () => {
|
||||
getHeldCapabilities.mockResolvedValue(new Set([SystemCapabilities.ACCESS_ADMIN]));
|
||||
|
||||
const result = await createResolver()(validParams);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(resolveSessionUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts an admin with section-specific Langfuse config access', async () => {
|
||||
getHeldCapabilities.mockResolvedValue(
|
||||
new Set([SystemCapabilities.ACCESS_ADMIN, configCapability('langfuse')]),
|
||||
);
|
||||
|
||||
const result = await createResolver()(validParams);
|
||||
|
||||
expect(result).toBe('https://cloud.langfuse.com/session-1');
|
||||
});
|
||||
|
||||
it('treats the system tenant and an omitted tenant as the same deployment', async () => {
|
||||
const result = await createResolver()({
|
||||
...validParams,
|
||||
viewer: { id: 'admin-1', role: 'ADMIN' },
|
||||
shareTenantId: '__SYSTEM__',
|
||||
});
|
||||
|
||||
expect(result).toBe('https://cloud.langfuse.com/session-1');
|
||||
});
|
||||
});
|
||||
81
packages/api/src/shared-links/session.ts
Normal file
81
packages/api/src/shared-links/session.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { configCapability, SYSTEM_TENANT_ID, SystemCapabilities } from '@librechat/data-schemas';
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
import type { CapabilityUser, GetHeldCapabilitiesFn } from '~/middleware/capabilities';
|
||||
import type { LangfuseSessionLinkParams } from '~/langfuse/session';
|
||||
import { resolveLangfuseSessionUrl } from '~/langfuse/session';
|
||||
|
||||
interface SharedSessionViewer {
|
||||
id?: string;
|
||||
_id?: { toString(): string };
|
||||
role?: string;
|
||||
tenantId?: string;
|
||||
idOnTheSource?: string | null;
|
||||
}
|
||||
|
||||
export interface SharedLangfuseSessionParams {
|
||||
viewer?: SharedSessionViewer;
|
||||
shareTenantId?: string;
|
||||
shareConversationId?: string;
|
||||
shareOwnerId?: string;
|
||||
config: TCustomConfig['langfuse'];
|
||||
}
|
||||
|
||||
export interface SharedLangfuseSessionDeps {
|
||||
getHeldCapabilities: GetHeldCapabilitiesFn;
|
||||
getMessages: LangfuseSessionLinkParams['getMessages'];
|
||||
resolveSessionUrl?: (params: LangfuseSessionLinkParams) => Promise<string | null>;
|
||||
}
|
||||
|
||||
function normalizeTenantId(tenantId?: string): string | undefined {
|
||||
return tenantId && tenantId !== SYSTEM_TENANT_ID ? tenantId : undefined;
|
||||
}
|
||||
|
||||
export function createSharedLangfuseSessionResolver(deps: SharedLangfuseSessionDeps) {
|
||||
const resolveSessionUrl = deps.resolveSessionUrl ?? resolveLangfuseSessionUrl;
|
||||
|
||||
return async function getSharedLangfuseSessionUrl({
|
||||
viewer,
|
||||
shareTenantId,
|
||||
shareConversationId,
|
||||
shareOwnerId,
|
||||
config,
|
||||
}: SharedLangfuseSessionParams): Promise<string | null> {
|
||||
const userId = viewer?.id ?? viewer?._id?.toString();
|
||||
if (
|
||||
!viewer ||
|
||||
!userId ||
|
||||
normalizeTenantId(viewer?.tenantId) !== normalizeTenantId(shareTenantId) ||
|
||||
!shareOwnerId ||
|
||||
!shareConversationId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const capabilityUser: CapabilityUser = {
|
||||
id: userId,
|
||||
role: viewer.role ?? '',
|
||||
tenantId: viewer.tenantId,
|
||||
idOnTheSource: viewer.idOnTheSource ?? null,
|
||||
};
|
||||
const langfuseConfigCapability = configCapability('langfuse');
|
||||
const heldCapabilities = await deps.getHeldCapabilities(capabilityUser, [
|
||||
SystemCapabilities.ACCESS_ADMIN,
|
||||
SystemCapabilities.MANAGE_CONFIGS,
|
||||
langfuseConfigCapability,
|
||||
]);
|
||||
if (
|
||||
!heldCapabilities.has(SystemCapabilities.ACCESS_ADMIN) ||
|
||||
(!heldCapabilities.has(SystemCapabilities.MANAGE_CONFIGS) &&
|
||||
!heldCapabilities.has(langfuseConfigCapability))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolveSessionUrl({
|
||||
config,
|
||||
conversationId: shareConversationId,
|
||||
userId: shareOwnerId,
|
||||
getMessages: deps.getMessages,
|
||||
});
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue