From ddb7090d6412e2ccda042fb61d46af5df9acfac7 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 12 May 2026 15:27:25 -0400 Subject: [PATCH] fix: preserve Langfuse settings when clearing secrets --- api/server/controllers/agents/v1.spec.js | 39 +++++++++++++++++++++++ packages/api/src/agents/langfuse.spec.ts | 40 ++++++++++++++++++++++++ packages/api/src/agents/langfuse.ts | 22 ++++++++++--- 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/api/server/controllers/agents/v1.spec.js b/api/server/controllers/agents/v1.spec.js index 2ca494e3cb..a0e54932fb 100644 --- a/api/server/controllers/agents/v1.spec.js +++ b/api/server/controllers/agents/v1.spec.js @@ -919,6 +919,45 @@ describe('Agent Controllers - Mass Assignment Protection', () => { }); }); + test('should preserve existing Langfuse settings when update only clears the secret', async () => { + const encryptedOriginal = await encryptStoredSecret('sk-original'); + await Agent.updateOne( + { id: existingAgentId }, + { + langfuse: { + enabled: true, + publicKey: 'pk-original', + secretKey: encryptedOriginal, + baseUrl: 'https://cloud.langfuse.com', + }, + }, + ); + + mockReq.params.id = existingAgentId; + mockReq.body = { + langfuse: { + secretKey: LANGFUSE_SECRET_CLEAR_VALUE, + }, + }; + + await updateAgentHandler(mockReq, mockRes); + + expect(mockRes.json).toHaveBeenCalled(); + const updatedAgent = mockRes.json.mock.calls[0][0]; + expect(updatedAgent.langfuse).toEqual({ + enabled: true, + publicKey: 'pk-original', + baseUrl: 'https://cloud.langfuse.com', + }); + + const agentInDb = await Agent.findOne({ id: existingAgentId }).lean(); + expect(agentInDb.langfuse).toEqual({ + enabled: true, + publicKey: 'pk-original', + baseUrl: 'https://cloud.langfuse.com', + }); + }); + test('uploadAgentAvatarHandler should redact Langfuse secret in response', async () => { await Agent.updateOne( { id: existingAgentId }, diff --git a/packages/api/src/agents/langfuse.spec.ts b/packages/api/src/agents/langfuse.spec.ts index 108d52b322..869ec8ba81 100644 --- a/packages/api/src/agents/langfuse.spec.ts +++ b/packages/api/src/agents/langfuse.spec.ts @@ -38,6 +38,46 @@ describe('normalizeLangfuseConfig', () => { publicKey: 'pk-agent', }); }); + + it('preserves existing non-secret fields when clearing only the secret', async () => { + const result = await normalizeLangfuseConfig( + { + secretKey: LANGFUSE_SECRET_CLEAR_VALUE, + }, + { + enabled: true, + publicKey: 'pk-agent', + secretKey: '0123456789abcdef0123456789abcdef:736b2d6167656e74', + baseUrl: 'https://cloud.langfuse.com', + }, + ); + + expect(result).toEqual({ + enabled: true, + publicKey: 'pk-agent', + baseUrl: 'https://cloud.langfuse.com', + }); + }); + + it('clears explicit blank non-secret fields while preserving absent fields', async () => { + const result = await normalizeLangfuseConfig( + { + publicKey: '', + secretKey: LANGFUSE_SECRET_CLEAR_VALUE, + }, + { + enabled: true, + publicKey: 'pk-agent', + secretKey: '0123456789abcdef0123456789abcdef:736b2d6167656e74', + baseUrl: 'https://cloud.langfuse.com', + }, + ); + + expect(result).toEqual({ + enabled: true, + baseUrl: 'https://cloud.langfuse.com', + }); + }); }); describe('redactLangfuseSecret', () => { diff --git a/packages/api/src/agents/langfuse.ts b/packages/api/src/agents/langfuse.ts index b1ac2141e2..14b9adc639 100644 --- a/packages/api/src/agents/langfuse.ts +++ b/packages/api/src/agents/langfuse.ts @@ -12,6 +12,10 @@ function isRecord(value: unknown): value is Record { return value != null && typeof value === 'object' && !Array.isArray(value); } +function hasOwn(value: Record, key: string): boolean { + return Object.prototype.hasOwnProperty.call(value, key); +} + function toPlainObject(value: unknown): unknown { if (!isRecord(value) || typeof value.toObject !== 'function') { return value; @@ -57,14 +61,24 @@ export async function normalizeLangfuseConfig( const existingLangfuse = isRecord(existingConfig) ? existingConfig : {}; const normalized: LangfuseConfig = {}; - if (typeof incoming.enabled === 'boolean') { + if (hasOwn(incoming, 'enabled') && typeof incoming.enabled === 'boolean') { normalized.enabled = incoming.enabled; + } else if (typeof existingLangfuse.enabled === 'boolean') { + normalized.enabled = existingLangfuse.enabled; } for (const key of ['publicKey', 'baseUrl'] as const) { - const value = incoming[key]; - if (isNonEmptyString(value)) { - normalized[key] = value.trim(); + if (hasOwn(incoming, key)) { + const value = incoming[key]; + if (isNonEmptyString(value)) { + normalized[key] = value.trim(); + } + continue; + } + + const existingValue = existingLangfuse[key]; + if (isNonEmptyString(existingValue)) { + normalized[key] = existingValue.trim(); } }