From b0a0ecf68323646e60fd9ff4e003d56d95fcf41d Mon Sep 17 00:00:00 2001 From: Om Date: Wed, 12 Aug 2026 18:16:42 +0530 Subject: [PATCH] fix(frontend): guard against a missing environment key in log decryption envKeys.find(...) can legitimately return undefined: a log entry can reference an environment that no longer has a matching key locally, for example audit history retained for an environment the viewer has since lost access to, or one that was deleted. The non-null assertion on that result would throw inside an async function called with no .catch() and no surrounding try/catch, an unhandled rejection with the same shape as the bug fixed in #970. Guard against the missing case explicitly and log it instead of asserting non-null, and wrap the decrypt call itself in a try/catch so a genuine decrypt failure for one log entry doesn't do the same thing. I looked into whether envKeys should be a dependency of this effect, since it's read but not listed. It shouldn't: envKeys is state on the enclosing SecretLogs component, and LogRow is defined inside that component's render body, so a change to envKeys already produces a fresh LogRow closure and resets this effect regardless of what's in its dependency array. Confirmed via eslint's own exhaustive-deps message after trying it. Added a comment and the disable directive so this isn't re-litigated later, but did not change that behaviour. --- frontend/components/logs/SecretLogs.tsx | 31 +++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/frontend/components/logs/SecretLogs.tsx b/frontend/components/logs/SecretLogs.tsx index 2f683ad4e..e707e11d1 100644 --- a/frontend/components/logs/SecretLogs.tsx +++ b/frontend/components/logs/SecretLogs.tsx @@ -239,15 +239,38 @@ export default function SecretLogs(props: { app: string }) { const envKeyPair = envKeys.find((envKey) => envKey.envId === event.environment.id) - const { publicKey, privateKey } = envKeyPair!.keys + // A log entry can reference an environment that no longer has a + // matching entry here, e.g. audit history retained for an + // environment the viewer has since lost access to or that was + // deleted. That is a real, expected case rather than a bug, so + // this must not be a non-null assertion: dereferencing undefined + // would throw inside this async function with nothing to catch + // it, an unhandled rejection. + if (!envKeyPair) { + console.error( + `No environment key available to decrypt log for environment ${event.environment.id}; leaving this entry undecrypted.` + ) + return + } + + const { publicKey, privateKey } = envKeyPair.keys - // Decrypt event fields - decryptedEvent!.key = await decryptAsymmetric(event!.key, privateKey, publicKey) + try { + // Decrypt event fields + decryptedEvent!.key = await decryptAsymmetric(event!.key, privateKey, publicKey) - setDecryptedEvent(decryptedEvent) + setDecryptedEvent(decryptedEvent) + } catch (error) { + console.error(`Failed to decrypt log for environment ${event.environment.id}:`, error) + } } if (log && envKeys.length > 0) decryptSecretEvent() + // envKeys is intentionally not a dependency: it is state on the + // enclosing SecretLogs component, so a change to it already re-renders + // this row with a fresh LogRow closure (LogRow is defined inside + // SecretLogs), which resets this effect regardless of its deps. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [log]) const relativeTimeStamp = () => {