From eed57cd10b6b364f5ccda58a3565f688d0caeb20 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 12 Jul 2026 19:34:25 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20API=20keys=20always=20showed=20"never=20?= =?UTF-8?q?used"=20=E2=80=94=20middleware=20blocked=20them=20entirely?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proxy.ts required a session cookie for every non-public /api/v1/* request, rejecting with 401 before the request ever reached requireSessionOrApiKey (lib/api-auth.ts) — the only place that actually verifies a Bearer API key and updates lastUsedAt. Pure API-key clients never send a session cookie, so every single API-key request was blocked at the middleware layer; the lastUsedAt update code was correct but unreachable. Now lets requests with an `Authorization: Bearer ek_...` header through to the route, which still does the real verification (and 401s itself on an invalid/unknown key) — middleware just stops pre-emptively rejecting valid ones. Also added error logging to the fire-and-forget lastUsedAt update, previously silent on failure. Verified locally: hashed a raw key, confirmed it matched the stored hash (so the lookup itself was never the problem), reproduced the 401 against the unpatched middleware, then confirmed both the 200 response and lastUsedAt populating correctly after the fix — visible in the real Settings → API Keys UI. --- apps/web/lib/api-auth.ts | 3 ++- apps/web/proxy.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/web/lib/api-auth.ts b/apps/web/lib/api-auth.ts index 9e0cccb..b0d8319 100644 --- a/apps/web/lib/api-auth.ts +++ b/apps/web/lib/api-auth.ts @@ -67,7 +67,8 @@ export async function requireSessionOrApiKey( void db .update(apiKeys) .set({ lastUsedAt: new Date() }) - .where(eq(apiKeys.id, keyRow.id)); + .where(eq(apiKeys.id, keyRow.id)) + .catch((err) => console.error("[api-auth] failed to update apiKeys.lastUsedAt", err)); const [user] = await db .select({ diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts index 761d6ef..1822d13 100644 --- a/apps/web/proxy.ts +++ b/apps/web/proxy.ts @@ -13,9 +13,16 @@ export async function proxy(request: NextRequest) { if (isPublic) return NextResponse.next(); + // API-key clients authenticate via `Authorization: Bearer ek_...`, not a + // session cookie — they'd otherwise be rejected here before ever reaching + // requireSessionOrApiKey (lib/api-auth.ts), which is the only place that + // actually verifies the key. Defer to it instead of requiring a cookie. + const authHeader = request.headers.get("authorization"); + const hasApiKeyHeader = isApi && authHeader?.startsWith("Bearer ek_"); + const sessionCookie = getSessionCookie(request); - if (!sessionCookie) { + if (!sessionCookie && !hasApiKeyHeader) { if (isApi) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }