From f7c514f49e956acd13ba787bd1f2bf5b7ac2a328 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Fri, 3 Jul 2026 13:57:31 -0500 Subject: [PATCH] fix(keys): ephemeral key TTL 15m -> 60m (long runs outlive the key, chat#1839) The TTL is only the backstop: runAgentWorkflow revokes the key on run end via deleteEphemeralKeyStep, so this does not extend key life on normal completion. But a runAgentStep timeout retries the whole agent loop (~10+ min per attempt), and on 2026-07-03 a customer run's final sends all 401'd on the expired key after 3 step retries (email_send_log 'rejected' rows with null account_id). 60 minutes covers several retry cycles; the chat#1839 refresh mechanism remains the proper long-term fix. Co-Authored-By: Claude Fable 5 --- lib/keys/__tests__/mintEphemeralAccountKey.test.ts | 6 +++++- lib/keys/mintEphemeralAccountKey.ts | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/keys/__tests__/mintEphemeralAccountKey.test.ts b/lib/keys/__tests__/mintEphemeralAccountKey.test.ts index 1c7ba361..57572ff6 100644 --- a/lib/keys/__tests__/mintEphemeralAccountKey.test.ts +++ b/lib/keys/__tests__/mintEphemeralAccountKey.test.ts @@ -20,7 +20,11 @@ vi.mock("@/lib/const", () => ({ PRIVY_PROJECT_SECRET: "secret" })); describe("mintEphemeralAccountKey", () => { beforeEach(() => vi.clearAllMocks()); - it("mints an account-scoped recoup_sk_ key with a ~15m expiry and returns rawKey + keyId", async () => { + it("defaults the ephemeral TTL to 60 minutes (long runs with step retries outlive 15m — chat#1839)", () => { + expect(DEFAULT_EPHEMERAL_KEY_TTL_MS).toBe(60 * 60 * 1000); + }); + + it("mints an account-scoped recoup_sk_ key with the default expiry and returns rawKey + keyId", async () => { vi.mocked(insertApiKey).mockResolvedValue({ data: { id: "key-1" }, error: null } as never); const before = Date.now(); diff --git a/lib/keys/mintEphemeralAccountKey.ts b/lib/keys/mintEphemeralAccountKey.ts index 8b138f27..52144b90 100644 --- a/lib/keys/mintEphemeralAccountKey.ts +++ b/lib/keys/mintEphemeralAccountKey.ts @@ -3,8 +3,16 @@ import { hashApiKey } from "@/lib/keys/hashApiKey"; import { insertApiKey } from "@/lib/supabase/account_api_keys/insertApiKey"; import { PRIVY_PROJECT_SECRET } from "@/lib/const"; -/** Default lifetime for an ephemeral key: 15 minutes. */ -export const DEFAULT_EPHEMERAL_KEY_TTL_MS = 15 * 60 * 1000; +/** + * Default lifetime for an ephemeral key: 60 minutes. The TTL is only the + * backstop — `runAgentWorkflow` revokes the key the moment the run ends + * (`deleteEphemeralKeyStep`), so a longer TTL does not extend key life on + * normal completion. 15 minutes proved too short for real runs: a + * `runAgentStep` timeout retries the whole agent loop (~10+ min per attempt), + * and on 2026-07-03 a customer run's final sends all 401'd on the expired key + * (chat#1839). 60 minutes covers several retry cycles. + */ +export const DEFAULT_EPHEMERAL_KEY_TTL_MS = 60 * 60 * 1000; export type EphemeralAccountKey = { rawKey: string; keyId: string };